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 identity<T>(arg: T): T { return arg; } // 调用时自动推导类型 let output1 = identity("hello"); // 类型为 string let output2 = identity(42); // 类型为 number<T>表示声明一个类型变量T,它会在函数调用时被具体的类型(如string、number)替换。
三、泛型函数
3.1 基本语法与使用
泛型函数在参数列表前使用<T>(可以用任何标识符,通常用T、U、K、V)。
function getArrayLength<T>(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 = identity<string>("hello"); // 显式指定 T = string手动指定在参数不足以推导时很有用(例如没有参数,或类型需要精确控制)。
3.3 多个类型参数
可以同时使用多个类型变量:
function merge<T, 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 Box<T> { value: T; getValue(): T; } const stringBox: Box<string> = { value: "hello", getValue() { return this.value; } }; const numberBox: Box<number> = { value: 100, getValue() { return this.value; } };泛型接口也常用于定义函数类型:
interface Comparator<T> { (a: T, b: T): number; } const compareNumbers: Comparator<number> = (a, b) => a - b;4.2 泛型类
类和接口类似,可以在类名后加上<T>。
class Stack<T> { 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 Stack<number>(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop()); // 2 (类型为 number | undefined) const stringStack = new Stack<string>(); stringStack.push("a");静态成员不能引用类的类型参数,因为静态成员属于类本身,而非实例。
五、泛型约束(Constraints)
有时候我们希望类型变量必须满足某些条件(比如必须有length属性)。这时可以使用extends关键字来约束。
5.1 基本约束
interface Lengthwise { length: number; } function logLength<T 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 getProperty<T, 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" | "age"keyof T是一个联合类型,包含 T 的所有公共属性名。
六、泛型默认类型
我们可以为泛型参数指定默认类型,类似函数参数的默认值。当调用者不指定时,使用默认类型。
interface ApiResponse<T = 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 Readonly<T> = { readonly [P in keyof T]: T[P]; }; type Partial<T> = { [P in keyof T]?: T[P]; };其实 TypeScript 内置了这些工具类型(见后文)。我们可以自己实现一个简单的映射类型,把所有属性变成null或undefined:
type Nullable<T> = { [P in keyof T]: T[P] | null; }; interface User { id: number; name: string; } type NullableUser = Nullable<User>; // 等价于 { id: number | null; name: string | null; }7.2 映射修饰符
readonly和?是映射类型中的修饰符。我们可以通过前缀+或-来添加或移除修饰符(+是默认的)。
// 移除所有属性的 readonly type Mutable<T> = { -readonly [P in keyof T]: T[P]; }; // 移除所有属性的可选修饰符(变成必选) type Required<T> = { [P in keyof T]-?: T[P]; };八、条件类型(Conditional Types)
条件类型类似于 JavaScript 的三元运算符:T extends U ? X : Y。它根据类型关系选择不同的类型。
8.1 基本语法
type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<number>; // false8.2 分布式条件类型
当条件类型作用于泛型且该泛型是联合类型时,TS 会将联合类型的每个成员分别代入条件,最后合并结果。这称为分布式条件类型。
type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never) // 结果: string[] | number[]防止分布式:用方括号包裹[T]。
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never; type Result2 = ToArrayNonDist<string | number>; // (string | number)[]8.3 infer 关键字
infer允许我们在条件类型中声明一个待推断的类型变量,常用于提取类型的内部结构。
// 获取函数返回值类型 type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function foo(): boolean { return true; } type FooReturn = ReturnType<typeof foo>; // boolean // 获取数组元素类型 type ElementType<T> = T extends (infer U)[] ? U : T; type E1 = ElementType<number[]>; // number type E2 = ElementType<string>; // string (不变)infer也可以用于元组和 Promise 等。
九、内置工具类型详解
TypeScript 内置了许多常用的类型工具,极大提升了开发效率。下面逐一介绍。
9.1Partial<T>—— 所有属性变为可选
interface Todo { title: string; description: string; completed: boolean; } function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo { return { ...todo, ...fieldsToUpdate }; } const todo1: Todo = { title: "Learn TS", description: "Study", completed: false }; const todo2 = updateTodo(todo1, { description: "Study advanced" });9.2Required<T>—— 所有属性变为必选
interface Props { a?: number; b?: string; } const obj: Required<Props> = { a: 5, b: "hello" }; // 必须提供 a 和 b9.3Readonly<T>—— 所有属性变为只读
const frozen: Readonly<Todo> = { title: "Freeze", description: "Immutable", completed: false }; // frozen.title = "Changed"; // ❌9.4Pick<T, K>—— 从 T 中挑选部分属性
type TodoPreview = Pick<Todo, "title" | "completed">; // { title: string; completed: boolean; }9.5Omit<T, K>—— 从 T 中排除部分属性
type TodoInfo = Omit<Todo, "completed">; // { title: string; description: string; }9.6Record<K, T>—— 构造一个对象类型,键为 K,值为 T
type PageInfo = { title: string; url: string; }; type Page = "home" | "about" | "contact"; const pages: Record<Page, PageInfo> = { home: { title: "Home", url: "/" }, about: { title: "About", url: "/about" }, contact: { title: "Contact", url: "/contact" } };9.7Exclude<T, U>—— 从 T 中排除可赋值给 U 的类型
type T = Exclude<"a" | "b" | "c", "a" | "b">; // "c"9.8Extract<T, U>—— 提取 T 中可赋值给 U 的类型
type T = Extract<"a" | "b" | "c", "a" | "d">; // "a"9.9NonNullable<T>—— 排除 null 和 undefined
type T = NonNullable<string | number | null | undefined>; // string | number9.10ReturnType<T>—— 获取函数返回值类型
function getString(): string { return "hello"; } type R = ReturnType<typeof getString>; // string9.11Parameters<T>—— 获取函数参数类型(元组)
function greet(name: string, age: number): void {} type Params = Parameters<typeof greet>; // [string, number]十、总结
本文深入讲解了 TypeScript 的高级类型特性:
泛型:
让类型变量化,编写可复用的组件
泛型函数、泛型接口、泛型类
泛型约束(
extends+keyof)泛型默认类型
映射类型:
基于旧类型通过
[P in keyof T]生成新类型修饰符
readonly、?及加减操作
条件类型:
T extends U ? X : Y分布式条件类型(联合类型自动分发)
infer提取类型
内置工具类型:
Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、ReturnType、Parameters等
这些高级特性是 TypeScript 区别于普通类型检查器的核心优势,也是写出健壮、灵活、可维护代码的关键。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。
