TypeScript 泛型讲解
如果说 TypeScript 是一门对类型进行编程的语言,那么泛型就是这门语言里的(函数)参数。本章,我将会从多角度讲解 TypeScript 中无处不在的泛型,以及它在类型别名、对象类型、函数与 Class 中的使用方式。
一、泛型的核心概念
1.基本定义
泛型(Generics)是 TypeScript 中允许在定义函数、接口或类时不预先指定具体类型,而是在使用时动态指定类型的机制。其核心目标是实现代码的可重用性与类型安全。
- 示例:
function identity<T>(arg: T): T { return arg; }
此处
<T>
为类型参数,T
在调用时被具体类型替换,如identity<string>("hello")
。 -
不过上述例子中直接
identity("hello") 也是可以的,
省略不写类型参数的值,让 TypeScript 自己推断。但有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。function comb<T>(arr1:T[], arr2:T[]):T[] {return arr1.concat(arr2); }comb([1, 2], ['a', 'b']) // 报错comb<number|string>([1, 2], ['a', 'b']) // 正确
上面示例中,两个参数
arr1
、arr2
和返回值都是同一个类型。如果不给出类型参数的值,调用会报错。如果类型参数是一个联合类型,就不会报错。
2. 泛型 vs any
any
的缺陷:放弃类型检查,失去 TypeScript 的类型安全优势。- 泛型的优势:保留类型信息,编译器可进行静态检查,如类型推断与错误提示 。
二、泛型的主要应用场景
泛型主要用在四个场合:函数、接口、类和别名。
1. 函数的泛型写法
通过泛型定义可处理多种类型的函数,避免重复代码:
function reverse<T>(items: T[]): T[] {return items.reverse();
}
const numbers = reverse([1, 2, 3]); // 推断为 number[]
const strings = reverse(["a", "b"]); // 推断为 string[]
此例中,T
自动匹配输入数组类型,返回值类型与输入一致。
2. 接口的泛型写法
定义灵活的类型契约,适用于容器类场景:
interface KeyValuePair<K, V> {key: K;value: V;
}
const pair: KeyValuePair<number, string> = { key: 1, value: "one" };
接口通过类型参数 K
和 V
支持多种键值组合。
3. 类的泛型写法
创建可复用的数据结构(如集合、栈、队列):
class Stack<T> {private items: T[] = [];push(item: T) { this.items.push(item); }pop(): T | undefined { return this.items.pop(); }
}
const numberStack = new Stack<number>();
numberStack.push(42); // 仅允许 number 类型
此类实现保证了栈内元素的类型一致性。
4. 类型别名的泛型写法
type 命令定义的类型别名,也可以使用泛型。
type Nullable<T> = T | undefined | null;
上面示例中,Nullable<T>
是一个泛型,只要传入一个类型,就可以得到这个类型与undefined
和null
的一个联合类型。
三、高级泛型技巧
1. 泛型约束
通过 extends
限制类型参数的范围:
interface HasLength { length: number; }
function logLength<T extends HasLength>(arg: T): void {console.log(arg.length);
}
logLength("hello"); // 合法(length=5)
logLength(42); // 错误:缺少 length 属性
此约束确保类型参数必须包含指定属性。
2. 多类型参数与默认值
多类型参数:
function swap<T, U>(tuple: [T, U]): [U, T] {return [tuple[1], tuple[0]];
}
swap([7, "seven"]); // 返回 ["seven", 7]
但是类型参数越少越好,下面我会讲到
默认类型:
class Generic<T = string> {list:T[] = []add(t:T) {this.list.push(t)}
}const g = new Generic();
g.add(4) // 报错
g.add('hello') // 正确-------------------------------------------const g = new Generic<number>();
g.add(4) // 正确
g.add('hello') // 报错
上面示例中,类Generic
有一个类型参数T
,默认值为string
。这意味着,属性list
默认是一个字符串数组,方法add()
的默认参数是一个字符串。所以,向add()
方法传入一个数值会报错,传入字符串就不会。反之,传入字符串会报错。
3. 索引类型与 keyof
确保对象属性访问的安全性:
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}
const person = { name: "Alice", age: 30 };
getValue(person, "name"); // 合法
getValue(person, "gender"); // 错误:属性不存在。
4. 条件类型与映射类型
条件类型:根据条件选择类型:
type Check<T> = T extends string ? "string" : "not string";
type A = Check<"hello">; // "string"
映射类型:基于已有类型生成新类型:
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyPerson = Readonly<Person>; // 所有属性变为只读。
四、泛型的正确使用场景与注意点
1.正确使用场景
- 当需要在多个位置(参数、返回值、成员变量)之间建立类型约束时。
- 避免重复编写相似逻辑的类型特定代码(如不同数据类型的队列实现)。
2.注意点
1、尽量少用泛型。
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
2、类型参数越少越好。
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
function filter<T,Fn extends (arg:T) => boolean
>(arr:T[],func:Fn
): T[] {return arr.filter(func);
}
上面示例有两个类型参数,但是第二个类型参数 Fn
是不必要的,完全可以直接写在函数参数的类型声明里面。
function filter<T>(arr:T[],func:(arg:T) => boolean
): T[] {return arr.filter(func);
}
上面示例中,类型参数简化成了一个,效果与前一个示例是一样的。
3、类型参数需要出现两次。
如果类型参数在定义后只出现一次,那么很可能是不必要的。
function greet<Str extends string>(s:Str
) {console.log('Hello, ' + s);
}
上面示例中,类型参数Str
只在函数声明中出现一次(除了它的定义部分),这往往表明这个类型参数是不必要。
function greet(s:string) {console.log('Hello, ' + s);
}
上面示例把前面的类型参数省略了,效果与前一个示例是一样的。
也就是说,只有当类型参数用到两次或两次以上,才是泛型的适用场合。
4、泛型可以嵌套。
类型参数可以是另一个泛型。
type OrNull<Type> = Type|null;
type OneOrMany<Type> = Type|Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
上面示例中,最后一行的泛型OrNull
的类型参数,就是另一个泛型OneOrMany
。
五、实战应用案例
1. React 组件泛型
定义可接收多种 props 类型的组件:
interface ListProps<T> {items: T[];renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {return <div>{items.map(renderItem)}</div>;
}
// 使用
<List<number> items={[1, 2]} renderItem={(n) => <div>{n}</div>} />。
2. API 请求封装
利用泛型约束返回数据类型:
async function fetchData<T>(url: string): Promise<T> {const response = await fetch(url);return response.json() as T;
}
interface User { id: number; name: string; }
const users = await fetchData<User[]>("/api/users");。