JS TypeScript 类型系统高级:条件类型、映射类型与类型推断

各位观众老爷,大家好!今天咱们来聊聊 TypeScript 类型系统里那些“高大上”但又贼有用的东西:条件类型、映射类型,以及类型推断。别怕,虽然听起来有点吓人,但保证你听完之后,功力大增,写代码的时候腰杆子都能挺直几分。

开场白:类型体操的重要性

TypeScript 的类型系统,就像一个严谨的管家,帮你管理代码里的各种数据类型,防止出现一些低级错误。但有时候,我们需要更灵活、更强大的类型操作,才能应对复杂的场景。这时候,条件类型、映射类型和类型推断就派上用场了。它们就像是管家的“高级技能”,让你可以更精细地控制类型,写出更健壮、更易于维护的代码。

第一部分:条件类型 (Conditional Types)

想象一下,你有一个函数,它根据不同的输入类型,返回不同的结果类型。在 JavaScript 里,你可能需要写一堆 if...else 判断。但在 TypeScript 里,你可以用条件类型,用类型的方式来实现这种逻辑。

条件类型的语法很简单:

T extends U ? X : Y

翻译成人话就是:如果类型 T 可以赋值给类型 UTU 的子类型),那么结果类型就是 X,否则结果类型就是 Y

1.1 基本用法:判断类型

最简单的用法就是判断一个类型是否是另一个类型的子类型。比如:

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

type A = IsString<string>; // true
type B = IsString<number>; // false

这里,IsString 就是一个条件类型。它判断 T 是否是 string 类型。如果是,结果就是 true,否则就是 false

1.2 分发条件类型 (Distributive Conditional Types)

当条件类型中的 T 是一个联合类型时,它会变成一个“分发条件类型”。也就是说,它会对联合类型中的每一个成员分别进行判断,然后把结果合并成一个新的联合类型。

举个例子:

type ToArray<T> = T extends any ? T[] : never;

type NumberArray = ToArray<number>; // number[]
type StringArray = ToArray<string>; // string[]
type NumberOrStringArray = ToArray<number | string>; // number[] | string[]

这里,ToArray 将任何类型 T 转换成数组类型 T[]。当 Tnumber | string 时,它会分别对 numberstring 进行判断,得到 number[]string[],然后把它们合并成 number[] | string[]

注意: 只有当 T 是一个“裸类型参数”(直接使用 T,没有被包裹在其他类型里)时,才会触发分发条件类型。

1.3 infer 关键字:类型推断

条件类型最强大的地方在于它可以使用 infer 关键字来推断类型。infer 就像一个“变量”,你可以用它来捕获类型中的一部分。

例如,假设你有一个函数类型 (arg: string) => number,你想提取出参数类型 string。你可以这样做:

type ArgumentType<T> = T extends (arg: infer U) => any ? U : never;

type MyFunc = (arg: string) => number;
type ArgType = ArgumentType<MyFunc>; // string

这里,infer U 表示 TypeScript 会尝试推断出函数参数的类型,并把它赋值给 U。如果 T 符合 (arg: infer U) => any 这种函数类型,那么结果就是 U,否则就是 never

1.4 实际应用:提取函数返回值类型

infer 最常见的应用场景之一就是提取函数的返回值类型:

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

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetingReturnType = ReturnType<typeof greet>; // string

这里,(...args: any[]) => infer R 表示任何函数类型,infer R 会推断出返回值类型,并把它赋值给 R

1.5 多个 infer 的使用

条件类型里可以同时使用多个 infer。例如,提取 Promise 的 resolve 类型:

type PromiseResolveType<T> = T extends Promise<infer U> ? U : never;

async function fetchData(): Promise<number> {
  return 123;
}

type DataType = PromiseResolveType<ReturnType<typeof fetchData>>; // number

这里,我们先用 ReturnType 提取出 fetchData 的返回值类型 Promise<number>,然后用 PromiseResolveType 提取出 Promise 的 resolve 类型 number

1.6 嵌套条件类型

条件类型还可以嵌套使用,实现更复杂的逻辑。例如,判断一个类型是否是数组,如果是,提取数组元素的类型,否则返回原类型:

type ElementType<T> = T extends (infer U)[] ? U : T;

type NumberArrayElement = ElementType<number[]>; // number
type StringElement = ElementType<string>; // string

1.7 总结:条件类型是类型系统的瑞士军刀

条件类型是 TypeScript 类型系统里非常强大的工具,它可以让你根据不同的类型,进行不同的类型操作。结合 infer 关键字,你可以提取类型中的一部分,实现更灵活的类型推断。

第二部分:映射类型 (Mapped Types)

映射类型允许你基于已有的类型,创建一个新的类型。它可以对已有的类型的每一个属性进行转换,实现类型属性的批量修改。

2.1 基本用法:只读属性

最简单的映射类型是把一个类型的属性都变成只读的:

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

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

type ReadonlyPerson = Readonly<Person>;

// ReadonlyPerson 的类型是:
// {
//   readonly name: string;
//   readonly age: number;
// }

这里,keyof T 表示 T 的所有属性名组成的联合类型。[K in keyof T] 表示遍历 T 的每一个属性名 KT[K] 表示 T 的属性 K 的类型。readonly 关键字表示把属性 K 变成只读的。

2.2 可选属性

类似地,你可以把一个类型的属性都变成可选的:

type Partial<T> = {
  [K in keyof T]?: T[K];
};

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

type PartialPerson = Partial<Person>;

// PartialPerson 的类型是:
// {
//   name?: string;
//   age?: number;
// }

这里,? 关键字表示把属性 K 变成可选的。

2.3 移除只读和可选属性

你也可以移除类型的只读和可选属性:

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

type Required<T> = {
  [K in keyof T]-?: T[K];
};

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

type MutablePerson = Mutable<Person>;
type RequiredPerson = Required<Person>;

// MutablePerson 的类型是:
// {
//   name: string;
//   age?: number;
// }

// RequiredPerson 的类型是:
// {
//   name: string;
//   age: number;
// }

这里,-readonly 表示移除只读属性,-? 表示移除可选属性。

2.4 映射类型与条件类型结合

映射类型可以和条件类型结合使用,实现更复杂的类型转换。例如,把一个类型的所有属性都变成数组类型:

type ToArray<T> = {
  [K in keyof T]: T[K] extends any ? T[K][] : T[K];
};

interface Person {
  name: string;
  age: number;
  hobbies: string[];
}

type PersonArray = ToArray<Person>;

// PersonArray 的类型是:
// {
//   name: string[];
//   age: number[];
//   hobbies: string[][];
// }

这里,T[K] extends any ? T[K][] : T[K] 表示如果属性 K 的类型是任何类型,那么就把它变成数组类型 T[K][],否则保持原类型。

2.5 as 关键字:重映射键名

TypeScript 4.1 引入了 as 关键字,允许你在映射类型中修改键名。例如,把一个类型的所有属性名都变成大写:

type UppercaseKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

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

type UppercasePerson = UppercaseKeys<Person>;

// UppercasePerson 的类型是:
// {
//   NAME: string;
//   AGE: number;
// }

这里,Uppercase<string & K> 表示把属性名 K 转换成大写。string & K 是为了确保 K 是一个字符串类型,因为 Uppercase 只能用于字符串类型。

2.6 过滤属性

as 关键字还可以用于过滤属性。例如,只保留类型中类型为 string 的属性:

type StringProps<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

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

type StringPerson = StringProps<Person>;

// StringPerson 的类型是:
// {
//   name: string;
//   address: string;
// }

这里,T[K] extends string ? K : never 表示如果属性 K 的类型是 string,那么就保留属性名 K,否则就把它变成 nevernever 类型的属性会被 TypeScript 自动移除。

2.7 总结:映射类型是类型转换的利器

映射类型允许你基于已有的类型,创建新的类型。它可以对已有的类型的每一个属性进行转换,实现类型属性的批量修改。结合条件类型和 as 关键字,你可以实现更复杂的类型转换和过滤。

第三部分:类型推断 (Type Inference)

类型推断是 TypeScript 的一个核心特性。它可以根据上下文自动推断出变量的类型,减少你的类型注解工作。

3.1 基本类型推断

最基本的类型推断是根据变量的初始值来推断类型:

let age = 30; // age 的类型被推断为 number
let name = "John"; // name 的类型被推断为 string
let isAdult = true; // isAdult 的类型被推断为 boolean

3.2 函数返回值类型推断

TypeScript 可以根据函数的返回值来推断函数的返回值类型:

function add(a: number, b: number) {
  return a + b; // 返回值类型被推断为 number
}

const result = add(1, 2); // result 的类型被推断为 number

3.3 泛型类型推断

TypeScript 可以根据函数调用的参数类型来推断泛型类型:

function identity<T>(arg: T): T {
  return arg;
}

let myString = identity("hello"); // T 被推断为 string
let myNumber = identity(123); // T 被推断为 number

3.4 上下文类型推断

在某些情况下,TypeScript 可以根据上下文来推断类型。例如,在事件处理函数中:

document.addEventListener("click", (event) => {
  // event 的类型被推断为 MouseEvent
  console.log(event.clientX, event.clientY);
});

3.5 类型守卫

类型守卫是一种在运行时检查类型的技术,它可以帮助 TypeScript 缩小类型范围,实现更精确的类型推断。

常见的类型守卫有:

  • typeof:用于检查基本类型。
  • instanceof:用于检查对象是否是某个类的实例。
  • in:用于检查对象是否包含某个属性。
  • 自定义类型守卫:通过函数来判断类型。

例如:

function printLength(obj: string | number[]) {
  if (typeof obj === "string") {
    // 在这个代码块里,obj 的类型被缩小为 string
    console.log(obj.length);
  } else {
    // 在这个代码块里,obj 的类型被缩小为 number[]
    console.log(obj.length);
  }
}

3.6 总结:类型推断是提高开发效率的关键

类型推断可以减少你的类型注解工作,提高开发效率。但是,过度依赖类型推断可能会导致代码可读性降低,所以需要在类型注解和类型推断之间找到一个平衡点。

总结:类型系统的进阶之路

今天我们聊了 TypeScript 类型系统里的条件类型、映射类型和类型推断。这些都是高级的类型操作,可以帮助你写出更健壮、更易于维护的代码。

  • 条件类型:根据不同的类型,进行不同的类型操作。
  • 映射类型:基于已有的类型,创建新的类型,实现类型属性的批量修改。
  • 类型推断:根据上下文自动推断出变量的类型,减少你的类型注解工作。

掌握了这些高级技能,你就可以在 TypeScript 的类型世界里自由驰骋,写出更优雅、更强大的代码。

希望今天的讲座对你有所帮助!下次再见!

发表回复

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