摘要泛型是 TypeScript 最强大、最核心的高级特性之一。它允许我们编写“适用于广泛类型”的代码而不是为每个类型重复编写逻辑。本文将从泛型的动机讲起逐步深入到泛型函数、泛型接口、泛型类、泛型约束再到映射类型、条件类型以及 TS 内置的工具类型Partial、Required、Pick、Omit、Readonly、Record 等。一、前言在前四篇文章中我们已经掌握了 TypeScript 的基础类型、函数、接口、类以及面向对象编程。现在我们可以给大部分代码加上类型让编译器帮我们检查错误。然而你会遇到这样的场景编写一个通用函数比如“获取数组中第一个元素”。如果是数字数组返回number字符串数组返回string用户对象数组返回User。不使用泛型的话我们只能写多个重载或用any丢失类型信息。// 使用 any 类型不安全 function firstElement(arr: any[]): any { return arr[0]; } const num firstElement([1, 2, 3]); // num 类型是 any无法享受后续类型检查泛型就是解决这个问题的把“类型”也作为参数在调用时再确定具体类型。二、泛型的动机让“类型参数化”想象一下你写了一个函数identity它返回传入的参数本身。在 JavaScript 中很简单function identity(arg) { return arg; }但在 TypeScript 中如果要求类型安全你可能想为每个类型写一个版本function identityNumber(arg: number): number { return arg; } function identityString(arg: string): string { return arg; } // 不可能为所有类型都写一遍泛型允许我们定义一个类型变量Type Variable在调用时才填充function identityT(arg: T): T { return arg; } // 调用时自动推导类型 let output1 identity(hello); // 类型为 string let output2 identity(42); // 类型为 numberT表示声明一个类型变量T它会在函数调用时被具体的类型如string、number替换。三、泛型函数3.1 基本语法与使用泛型函数在参数列表前使用T可以用任何标识符通常用T、U、K、V。function getArrayLengthT(arr: T[]): number { return arr.length; } console.log(getArrayLength([1, 2, 3])); // T 被推导为 number console.log(getArrayLength([a, b, c])); // T 被推导为 string3.2 类型推导与显式指定大多数情况下TypeScript 能根据参数自动推导类型变量。你也可以手动指定let result identitystring(hello); // 显式指定 T string手动指定在参数不足以推导时很有用例如没有参数或类型需要精确控制。3.3 多个类型参数可以同时使用多个类型变量function mergeT, U(obj1: T, obj2: U): T U { return { ...obj1, ...obj2 }; } const merged merge({ name: Tom }, { age: 25 }); // merged 类型为 { name: string } { age: number } { name: string; age: number } console.log(merged.name, merged.age);四、泛型接口与泛型类4.1 泛型接口接口也可以使用泛型使其更灵活。interface BoxT { value: T; getValue(): T; } const stringBox: Boxstring { value: hello, getValue() { return this.value; } }; const numberBox: Boxnumber { value: 100, getValue() { return this.value; } };泛型接口也常用于定义函数类型interface ComparatorT { (a: T, b: T): number; } const compareNumbers: Comparatornumber (a, b) a - b;4.2 泛型类类和接口类似可以在类名后加上T。class StackT { private items: T[] []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } } const numberStack new Stacknumber(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 (类型为 number | undefined) const stringStack new Stackstring(); stringStack.push(a);静态成员不能引用类的类型参数因为静态成员属于类本身而非实例。五、泛型约束Constraints有时候我们希望类型变量必须满足某些条件比如必须有length属性。这时可以使用extends关键字来约束。5.1 基本约束interface Lengthwise { length: number; } function logLengthT extends Lengthwise(arg: T): T { console.log(arg.length); return arg; } logLength(hello); // ✅ 字符串有 length logLength([1, 2, 3]); // ✅ 数组有 length // logLength(123); // ❌ 数字没有 length 属性5.2 使用 keyof 约束属性名当你需要确保传入的键key确实存在于某个对象中时可以使用keyof操作符。function getPropertyT, K extends keyof T(obj: T, key: K): T[K] { return obj[key]; } const person { name: Alice, age: 30 }; const nameValue getProperty(person, name); // string const ageValue getProperty(person, age); // number // const invalid getProperty(person, gender); // ❌ 参数 gender 不能赋给 name | agekeyof T是一个联合类型包含 T 的所有公共属性名。六、泛型默认类型我们可以为泛型参数指定默认类型类似函数参数的默认值。当调用者不指定时使用默认类型。interface ApiResponseT any { code: number; data: T; message: string; } // 使用默认类型 any const res1: ApiResponse { code: 200, data: ok, message: success }; // 指定具体类型 const res2: ApiResponse{ id: number } { code: 200, data: { id: 1 }, message: success };默认类型在有可选参数或复杂层级时非常有用。七、映射类型Mapped Types映射类型允许你基于旧类型创建新类型通过对旧类型的每个属性进行转换。7.1 基础语法映射类型的语法是{ [P in K]: T }其中K是一个联合类型通常是keyof T。type ReadonlyT { readonly [P in keyof T]: T[P]; }; type PartialT { [P in keyof T]?: T[P]; };其实 TypeScript 内置了这些工具类型见后文。我们可以自己实现一个简单的映射类型把所有属性变成null或undefinedtype NullableT { [P in keyof T]: T[P] | null; }; interface User { id: number; name: string; } type NullableUser NullableUser; // 等价于 { id: number | null; name: string | null; }7.2 映射修饰符readonly和?是映射类型中的修饰符。我们可以通过前缀或-来添加或移除修饰符是默认的。// 移除所有属性的 readonly type MutableT { -readonly [P in keyof T]: T[P]; }; // 移除所有属性的可选修饰符变成必选 type RequiredT { [P in keyof T]-?: T[P]; };八、条件类型Conditional Types条件类型类似于 JavaScript 的三元运算符T extends U ? X : Y。它根据类型关系选择不同的类型。8.1 基本语法type IsStringT T extends string ? true : false; type A IsStringhello; // true type B IsStringnumber; // false8.2 分布式条件类型当条件类型作用于泛型且该泛型是联合类型时TS 会将联合类型的每个成员分别代入条件最后合并结果。这称为分布式条件类型。type ToArrayT T extends any ? T[] : never; type Result ToArraystring | number; // 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never) // 结果: string[] | number[]防止分布式用方括号包裹[T]。type ToArrayNonDistT [T] extends [any] ? T[] : never; type Result2 ToArrayNonDiststring | number; // (string | number)[]8.3 infer 关键字infer允许我们在条件类型中声明一个待推断的类型变量常用于提取类型的内部结构。// 获取函数返回值类型 type ReturnTypeT T extends (...args: any[]) infer R ? R : never; function foo(): boolean { return true; } type FooReturn ReturnTypetypeof foo; // boolean // 获取数组元素类型 type ElementTypeT T extends (infer U)[] ? U : T; type E1 ElementTypenumber[]; // number type E2 ElementTypestring; // string (不变)infer也可以用于元组和 Promise 等。九、内置工具类型详解TypeScript 内置了许多常用的类型工具极大提升了开发效率。下面逐一介绍。9.1PartialT—— 所有属性变为可选interface Todo { title: string; description: string; completed: boolean; } function updateTodo(todo: Todo, fieldsToUpdate: PartialTodo): Todo { return { ...todo, ...fieldsToUpdate }; } const todo1: Todo { title: Learn TS, description: Study, completed: false }; const todo2 updateTodo(todo1, { description: Study advanced });9.2RequiredT—— 所有属性变为必选interface Props { a?: number; b?: string; } const obj: RequiredProps { a: 5, b: hello }; // 必须提供 a 和 b9.3ReadonlyT—— 所有属性变为只读const frozen: ReadonlyTodo { title: Freeze, description: Immutable, completed: false }; // frozen.title Changed; // ❌9.4PickT, K—— 从 T 中挑选部分属性type TodoPreview PickTodo, title | completed; // { title: string; completed: boolean; }9.5OmitT, K—— 从 T 中排除部分属性type TodoInfo OmitTodo, completed; // { title: string; description: string; }9.6RecordK, T—— 构造一个对象类型键为 K值为 Ttype PageInfo { title: string; url: string; }; type Page home | about | contact; const pages: RecordPage, PageInfo { home: { title: Home, url: / }, about: { title: About, url: /about }, contact: { title: Contact, url: /contact } };9.7ExcludeT, U—— 从 T 中排除可赋值给 U 的类型type T Excludea | b | c, a | b; // c9.8ExtractT, U—— 提取 T 中可赋值给 U 的类型type T Extracta | b | c, a | d; // a9.9NonNullableT—— 排除 null 和 undefinedtype T NonNullablestring | number | null | undefined; // string | number9.10ReturnTypeT—— 获取函数返回值类型function getString(): string { return hello; } type R ReturnTypetypeof getString; // string9.11ParametersT—— 获取函数参数类型元组function greet(name: string, age: number): void {} type Params Parameterstypeof greet; // [string, number]十、总结本文深入讲解了 TypeScript 的高级类型特性泛型让类型变量化编写可复用的组件泛型函数、泛型接口、泛型类泛型约束extendskeyof泛型默认类型映射类型基于旧类型通过[P in keyof T]生成新类型修饰符readonly、?及加减操作条件类型T extends U ? X : Y分布式条件类型联合类型自动分发infer提取类型内置工具类型Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、ReturnType、Parameters等这些高级特性是 TypeScript 区别于普通类型检查器的核心优势也是写出健壮、灵活、可维护代码的关键。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。动手练习是掌握编程最快的方法请务必亲手敲一遍本文的所有示例代码并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源谢谢大家。
TypeScript 从零基础到精通(五):高级类型与泛型
摘要泛型是 TypeScript 最强大、最核心的高级特性之一。它允许我们编写“适用于广泛类型”的代码而不是为每个类型重复编写逻辑。本文将从泛型的动机讲起逐步深入到泛型函数、泛型接口、泛型类、泛型约束再到映射类型、条件类型以及 TS 内置的工具类型Partial、Required、Pick、Omit、Readonly、Record 等。一、前言在前四篇文章中我们已经掌握了 TypeScript 的基础类型、函数、接口、类以及面向对象编程。现在我们可以给大部分代码加上类型让编译器帮我们检查错误。然而你会遇到这样的场景编写一个通用函数比如“获取数组中第一个元素”。如果是数字数组返回number字符串数组返回string用户对象数组返回User。不使用泛型的话我们只能写多个重载或用any丢失类型信息。// 使用 any 类型不安全 function firstElement(arr: any[]): any { return arr[0]; } const num firstElement([1, 2, 3]); // num 类型是 any无法享受后续类型检查泛型就是解决这个问题的把“类型”也作为参数在调用时再确定具体类型。二、泛型的动机让“类型参数化”想象一下你写了一个函数identity它返回传入的参数本身。在 JavaScript 中很简单function identity(arg) { return arg; }但在 TypeScript 中如果要求类型安全你可能想为每个类型写一个版本function identityNumber(arg: number): number { return arg; } function identityString(arg: string): string { return arg; } // 不可能为所有类型都写一遍泛型允许我们定义一个类型变量Type Variable在调用时才填充function identityT(arg: T): T { return arg; } // 调用时自动推导类型 let output1 identity(hello); // 类型为 string let output2 identity(42); // 类型为 numberT表示声明一个类型变量T它会在函数调用时被具体的类型如string、number替换。三、泛型函数3.1 基本语法与使用泛型函数在参数列表前使用T可以用任何标识符通常用T、U、K、V。function getArrayLengthT(arr: T[]): number { return arr.length; } console.log(getArrayLength([1, 2, 3])); // T 被推导为 number console.log(getArrayLength([a, b, c])); // T 被推导为 string3.2 类型推导与显式指定大多数情况下TypeScript 能根据参数自动推导类型变量。你也可以手动指定let result identitystring(hello); // 显式指定 T string手动指定在参数不足以推导时很有用例如没有参数或类型需要精确控制。3.3 多个类型参数可以同时使用多个类型变量function mergeT, U(obj1: T, obj2: U): T U { return { ...obj1, ...obj2 }; } const merged merge({ name: Tom }, { age: 25 }); // merged 类型为 { name: string } { age: number } { name: string; age: number } console.log(merged.name, merged.age);四、泛型接口与泛型类4.1 泛型接口接口也可以使用泛型使其更灵活。interface BoxT { value: T; getValue(): T; } const stringBox: Boxstring { value: hello, getValue() { return this.value; } }; const numberBox: Boxnumber { value: 100, getValue() { return this.value; } };泛型接口也常用于定义函数类型interface ComparatorT { (a: T, b: T): number; } const compareNumbers: Comparatornumber (a, b) a - b;4.2 泛型类类和接口类似可以在类名后加上T。class StackT { private items: T[] []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } } const numberStack new Stacknumber(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 (类型为 number | undefined) const stringStack new Stackstring(); stringStack.push(a);静态成员不能引用类的类型参数因为静态成员属于类本身而非实例。五、泛型约束Constraints有时候我们希望类型变量必须满足某些条件比如必须有length属性。这时可以使用extends关键字来约束。5.1 基本约束interface Lengthwise { length: number; } function logLengthT extends Lengthwise(arg: T): T { console.log(arg.length); return arg; } logLength(hello); // ✅ 字符串有 length logLength([1, 2, 3]); // ✅ 数组有 length // logLength(123); // ❌ 数字没有 length 属性5.2 使用 keyof 约束属性名当你需要确保传入的键key确实存在于某个对象中时可以使用keyof操作符。function getPropertyT, K extends keyof T(obj: T, key: K): T[K] { return obj[key]; } const person { name: Alice, age: 30 }; const nameValue getProperty(person, name); // string const ageValue getProperty(person, age); // number // const invalid getProperty(person, gender); // ❌ 参数 gender 不能赋给 name | agekeyof T是一个联合类型包含 T 的所有公共属性名。六、泛型默认类型我们可以为泛型参数指定默认类型类似函数参数的默认值。当调用者不指定时使用默认类型。interface ApiResponseT any { code: number; data: T; message: string; } // 使用默认类型 any const res1: ApiResponse { code: 200, data: ok, message: success }; // 指定具体类型 const res2: ApiResponse{ id: number } { code: 200, data: { id: 1 }, message: success };默认类型在有可选参数或复杂层级时非常有用。七、映射类型Mapped Types映射类型允许你基于旧类型创建新类型通过对旧类型的每个属性进行转换。7.1 基础语法映射类型的语法是{ [P in K]: T }其中K是一个联合类型通常是keyof T。type ReadonlyT { readonly [P in keyof T]: T[P]; }; type PartialT { [P in keyof T]?: T[P]; };其实 TypeScript 内置了这些工具类型见后文。我们可以自己实现一个简单的映射类型把所有属性变成null或undefinedtype NullableT { [P in keyof T]: T[P] | null; }; interface User { id: number; name: string; } type NullableUser NullableUser; // 等价于 { id: number | null; name: string | null; }7.2 映射修饰符readonly和?是映射类型中的修饰符。我们可以通过前缀或-来添加或移除修饰符是默认的。// 移除所有属性的 readonly type MutableT { -readonly [P in keyof T]: T[P]; }; // 移除所有属性的可选修饰符变成必选 type RequiredT { [P in keyof T]-?: T[P]; };八、条件类型Conditional Types条件类型类似于 JavaScript 的三元运算符T extends U ? X : Y。它根据类型关系选择不同的类型。8.1 基本语法type IsStringT T extends string ? true : false; type A IsStringhello; // true type B IsStringnumber; // false8.2 分布式条件类型当条件类型作用于泛型且该泛型是联合类型时TS 会将联合类型的每个成员分别代入条件最后合并结果。这称为分布式条件类型。type ToArrayT T extends any ? T[] : never; type Result ToArraystring | number; // 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never) // 结果: string[] | number[]防止分布式用方括号包裹[T]。type ToArrayNonDistT [T] extends [any] ? T[] : never; type Result2 ToArrayNonDiststring | number; // (string | number)[]8.3 infer 关键字infer允许我们在条件类型中声明一个待推断的类型变量常用于提取类型的内部结构。// 获取函数返回值类型 type ReturnTypeT T extends (...args: any[]) infer R ? R : never; function foo(): boolean { return true; } type FooReturn ReturnTypetypeof foo; // boolean // 获取数组元素类型 type ElementTypeT T extends (infer U)[] ? U : T; type E1 ElementTypenumber[]; // number type E2 ElementTypestring; // string (不变)infer也可以用于元组和 Promise 等。九、内置工具类型详解TypeScript 内置了许多常用的类型工具极大提升了开发效率。下面逐一介绍。9.1PartialT—— 所有属性变为可选interface Todo { title: string; description: string; completed: boolean; } function updateTodo(todo: Todo, fieldsToUpdate: PartialTodo): Todo { return { ...todo, ...fieldsToUpdate }; } const todo1: Todo { title: Learn TS, description: Study, completed: false }; const todo2 updateTodo(todo1, { description: Study advanced });9.2RequiredT—— 所有属性变为必选interface Props { a?: number; b?: string; } const obj: RequiredProps { a: 5, b: hello }; // 必须提供 a 和 b9.3ReadonlyT—— 所有属性变为只读const frozen: ReadonlyTodo { title: Freeze, description: Immutable, completed: false }; // frozen.title Changed; // ❌9.4PickT, K—— 从 T 中挑选部分属性type TodoPreview PickTodo, title | completed; // { title: string; completed: boolean; }9.5OmitT, K—— 从 T 中排除部分属性type TodoInfo OmitTodo, completed; // { title: string; description: string; }9.6RecordK, T—— 构造一个对象类型键为 K值为 Ttype PageInfo { title: string; url: string; }; type Page home | about | contact; const pages: RecordPage, PageInfo { home: { title: Home, url: / }, about: { title: About, url: /about }, contact: { title: Contact, url: /contact } };9.7ExcludeT, U—— 从 T 中排除可赋值给 U 的类型type T Excludea | b | c, a | b; // c9.8ExtractT, U—— 提取 T 中可赋值给 U 的类型type T Extracta | b | c, a | d; // a9.9NonNullableT—— 排除 null 和 undefinedtype T NonNullablestring | number | null | undefined; // string | number9.10ReturnTypeT—— 获取函数返回值类型function getString(): string { return hello; } type R ReturnTypetypeof getString; // string9.11ParametersT—— 获取函数参数类型元组function greet(name: string, age: number): void {} type Params Parameterstypeof greet; // [string, number]十、总结本文深入讲解了 TypeScript 的高级类型特性泛型让类型变量化编写可复用的组件泛型函数、泛型接口、泛型类泛型约束extendskeyof泛型默认类型映射类型基于旧类型通过[P in keyof T]生成新类型修饰符readonly、?及加减操作条件类型T extends U ? X : Y分布式条件类型联合类型自动分发infer提取类型内置工具类型Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、ReturnType、Parameters等这些高级特性是 TypeScript 区别于普通类型检查器的核心优势也是写出健壮、灵活、可维护代码的关键。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。动手练习是掌握编程最快的方法请务必亲手敲一遍本文的所有示例代码并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源谢谢大家。