各位观众老爷,大家好!今天咱们来聊聊 TypeScript 类型系统里那些“高大上”但又贼有用的东西:条件类型、映射类型,以及类型推断。别怕,虽然听起来有点吓人,但保证你听完之后,功力大增,写代码的时候腰杆子都能挺直几分。
开场白:类型体操的重要性
TypeScript 的类型系统,就像一个严谨的管家,帮你管理代码里的各种数据类型,防止出现一些低级错误。但有时候,我们需要更灵活、更强大的类型操作,才能应对复杂的场景。这时候,条件类型、映射类型和类型推断就派上用场了。它们就像是管家的“高级技能”,让你可以更精细地控制类型,写出更健壮、更易于维护的代码。
第一部分:条件类型 (Conditional Types)
想象一下,你有一个函数,它根据不同的输入类型,返回不同的结果类型。在 JavaScript 里,你可能需要写一堆 if...else
判断。但在 TypeScript 里,你可以用条件类型,用类型的方式来实现这种逻辑。
条件类型的语法很简单:
T extends U ? X : Y
翻译成人话就是:如果类型 T
可以赋值给类型 U
(T
是 U
的子类型),那么结果类型就是 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[]
。当 T
是 number | string
时,它会分别对 number
和 string
进行判断,得到 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
的每一个属性名 K
。T[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
,否则就把它变成 never
。never
类型的属性会被 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 的类型世界里自由驰骋,写出更优雅、更强大的代码。
希望今天的讲座对你有所帮助!下次再见!