JS `TypeScript` 类型系统高级:泛型、条件类型与类型守卫

嘿,各位代码界的探险家们,欢迎来到今天的“JS TypeScript 类型系统高级探秘”讲座!今天咱们不搞虚的,直接上干货,一起解锁泛型、条件类型和类型守卫这些TypeScript的强大武器,让你的代码更加健壮、灵活,也让你的秃头进程稍微放缓那么一点点。

第一站:泛型——代码的变形金刚

首先,我们来聊聊泛型。啥是泛型?简单来说,你可以把它想象成一个“类型变量”,就像函数中的参数一样,只不过它代表的是类型。有了它,我们可以编写可以适用于多种类型的代码,避免写一堆重复的、类型不同的函数或类。

举个例子,假设我们需要一个函数,它能返回传入的任何类型的值,并附带一个描述信息。如果没有泛型,你可能需要写很多个函数,每个函数对应一种类型,像这样:

function identityString(arg: string): { value: string, message: string } {
  return { value: arg, message: "这是个字符串" };
}

function identityNumber(arg: number): { value: number, message: string } {
  return { value: arg, message: "这是个数字" };
}

// ... 更多类型

这样写,不仅代码冗余,而且如果以后需要支持新的类型,还得不断添加新的函数。这时候,泛型就派上用场了!

function identity<T>(arg: T): { value: T, message: string } {
  return { value: arg, message: "这是个神奇的值" };
}

let stringResult = identity<string>("Hello");
let numberResult = identity<number>(123);

console.log(stringResult); // { value: "Hello", message: "这是个神奇的值" }
console.log(numberResult); // { value: 123, message: "这是个神奇的值" }

在这个例子中,<T> 就定义了一个类型变量 T。当你调用 identity<string>("Hello") 时,T 就被推断为 string 类型。当你调用 identity<number>(123) 时,T 就被推断为 number 类型。是不是很酷?

泛型接口与类

泛型不仅可以用于函数,还可以用于接口和类。

interface GenericIdentityFn<T> {
  (arg: T): { value: T, message: string };
}

let myIdentity: GenericIdentityFn<number> = identity; // 假设 identity 函数已经定义

console.log(myIdentity(42)); // { value: 42, message: "这是个神奇的值" }

上面的代码定义了一个泛型接口 GenericIdentityFn,它描述了一个接受类型为 T 的参数,并返回一个包含类型为 Tvaluemessage 属性的对象的函数。

再来看看泛型类:

class DataHolder<T> {
  data: T;

  constructor(data: T) {
    this.data = data;
  }

  getData(): T {
    return this.data;
  }
}

let stringHolder = new DataHolder<string>("TypeScript");
let numberHolder = new DataHolder<number>(3.14);

console.log(stringHolder.getData()); // TypeScript
console.log(numberHolder.getData()); // 3.14

DataHolder 类可以存储任何类型的数据,并且通过 getData 方法获取。

泛型约束

有时候,我们希望泛型类型具有某些特定的属性或方法。这时,我们可以使用泛型约束。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);  // Now we know it has a .length property, so no more error
  return arg;
}

loggingIdentity("Hello"); // OK
loggingIdentity([1, 2, 3]); // OK
// loggingIdentity(123); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

在这个例子中,T extends Lengthwise 表示 T 必须满足 Lengthwise 接口的约束,也就是说,它必须有一个 length 属性。

总结一下泛型的好处:

优点 描述
代码复用性 编写一次,可以用于多种类型。
类型安全 在编译时进行类型检查,避免运行时错误。
更好的可读性 通过类型参数,可以更清晰地表达代码的意图。

第二站:条件类型——类型世界的 if-else

接下来,我们进入条件类型的世界。条件类型允许我们根据某个条件来选择不同的类型,就像代码中的 if-else 语句一样。

它的语法是这样的:T extends U ? X : Y。意思是如果类型 T 可以赋值给类型 U,那么结果类型就是 X,否则就是 Y

一个简单的例子:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // true
type NumberResult = IsString<number>; // false

IsString 类型接收一个类型参数 T,如果 Tstring 类型,那么结果就是 true,否则就是 false

infer 关键字

条件类型常常和 infer 关键字一起使用。infer 关键字允许我们在条件类型中推断类型变量。

例如,我们可以使用条件类型和 infer 来提取函数返回值的类型:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // number

在这个例子中,T extends (...args: any) => any 约束 T 必须是一个函数类型。然后,T extends (...args: any) => infer R ? R : any 使用 infer R 推断函数返回值的类型,并将其赋值给 R。如果 T 不是函数类型,那么结果就是 any

ExcludeExtract 类型

TypeScript 内置了一些非常有用的条件类型,例如 ExcludeExtract

Exclude<T, U> 用于从类型 T 中排除可以赋值给类型 U 的类型。

type T0 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;  // "c"
type T2 = Exclude<string | number | (() => void), Function>;  // string | number

Extract<T, U> 用于从类型 T 中提取可以赋值给类型 U 的类型。

type T3 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T4 = Extract<string | number | (() => void), Function>;  // () => void

应用场景:类型映射

条件类型还可以用于类型映射,例如,我们可以将一个对象的所有属性变为只读:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

// ReadonlyPerson 类型等价于:
// interface ReadonlyPerson {
//   readonly name: string;
//   readonly age: number;
// }

条件类型总结:

特性 描述
T extends U ? X : Y 根据 T 是否可以赋值给 U 来选择类型 XY
infer 用于在条件类型中推断类型变量。
Exclude<T, U> 从类型 T 中排除可以赋值给类型 U 的类型。
Extract<T, U> 从类型 T 中提取可以赋值给类型 U 的类型。

第三站:类型守卫——类型世界的侦探

最后,我们来学习类型守卫。类型守卫是一种在运行时缩小变量类型范围的技术。它可以帮助 TypeScript 更好地理解你的代码,并提供更准确的类型检查。

TypeScript 提供了几种类型守卫的方式:

  1. typeof 类型守卫

typeof 类型守卫用于判断变量的类型。

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript 知道 value 是 string 类型
  } else {
    console.log(value.toFixed(2)); // TypeScript 知道 value 是 number 类型
  }
}

printValue("hello");
printValue(123.456);
  1. instanceof 类型守卫

instanceof 类型守卫用于判断一个对象是否是某个类的实例。

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function makeSound(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark(); // TypeScript 知道 animal 是 Dog 类型
  } else {
    console.log("Generic animal sound");
  }
}

let dog = new Dog("Buddy");
let animal = new Animal("Generic animal");

makeSound(dog); // Woof!
makeSound(animal); // Generic animal sound
  1. 自定义类型守卫

我们可以使用 is 关键字来定义自定义类型守卫。

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function isBird(animal: Bird | Fish): animal is Bird {
  return (animal as Bird).fly !== undefined;
}

function doSomething(animal: Bird | Fish) {
  if (isBird(animal)) {
    animal.fly(); // TypeScript 知道 animal 是 Bird 类型
  } else {
    animal.swim(); // TypeScript 知道 animal 是 Fish 类型
  }
}

let myBird: Bird = {
  fly: () => console.log("Flying"),
  layEggs: () => console.log("Laying eggs")
};

let myFish: Fish = {
  swim: () => console.log("Swimming"),
  layEggs: () => console.log("Laying eggs")
};

doSomething(myBird); // Flying
doSomething(myFish); // Swimming

在这个例子中,isBird(animal: Bird | Fish): animal is Bird 定义了一个类型守卫函数。如果 isBird(animal) 返回 true,那么 TypeScript 就会认为 animalBird 类型。

类型守卫总结:

类型守卫方式 描述
typeof 判断变量的类型。
instanceof 判断一个对象是否是某个类的实例。
自定义类型守卫 使用 is 关键字定义自定义类型守卫函数。

总结

好了,各位代码界的探险家们,今天的“JS TypeScript 类型系统高级探秘”讲座就到这里。我们一起探索了泛型、条件类型和类型守卫这三大神器。希望这些知识能帮助你写出更健壮、更灵活、更易于维护的 TypeScript 代码。

记住,熟能生巧,多练习,多实践,你也能成为类型系统的大师!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注