TypeScript 泛型全解 —— 泛型函数、泛型接口、泛型类与约束
本文献给:
已掌握 TypeScript 基础类型、接口、类、函数类型等知识的开发者。本文将系统讲解 TypeScript 泛型的核心概念,包括泛型函数、泛型接口、泛型类、泛型约束、构造函数类型以及泛型参数推断,帮助你写出更灵活、更安全的可复用代码。
你将学到:
- 为什么需要泛型 —— 类型参数化的价值
- 泛型函数的基本写法与类型推断
- 泛型接口的定义与使用
- 泛型约束(
extends)与多个泛型变量 - 泛型中的
new与构造函数类型 - 泛型参数推断规则与常见陷阱
- 泛型默认类型
一、为什么需要泛型
1.1 重复类型的痛点
假设有一个函数,想要返回传入的值的原样(identity)。不使用类型时可能是 any,但失去类型安全。
function identity(value: any): any {
return value;
}
const result = identity("hello"); // result 类型为 any,丢失了 string 信息
使用联合类型无法覆盖所有类型,且不可扩展。
1.2 类型参数化
泛型允许我们定义类型参数(type parameter),在调用时传入具体类型。
function identity<T>(value: T): T {
return value;
}
const str = identity<string>("hello"); // 显式指定类型参数
const num = identity(42); // 类型推断为 number
T 是一个占位符,代表调用时传入的类型。返回值类型与参数类型相同,保持了类型关联。
二、泛型函数
2.1 基本语法
function wrapInArray<T>(value: T): T[] {
return [value];
}
const strArr = wrapInArray("hello"); // string[]
const numArr = wrapInArray(42); // number[]
2.2 多个泛型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p = pair("name", 25); // [string, number]
2.3 类型推断与显式指定
通常 TypeScript 能自动推断类型参数,不需要显式指定。
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // 推断为 number
当无法推断或需要明确类型时,可以显式指定。
function logSize<T>(arr: T[]): void {
console.log(arr.length);
}
// 显式指定,虽然此处没必要
logSize<string>(['a','b']);
2.4 泛型函数类型表达式
type Identity = <T>(value: T) => T;
const myIdentity: Identity = (x) => x;
也可以把泛型参数放在类型别名上(作用域不同,但效果类似)。
type Identity2<T> = (value: T) => T;
const myIdentity2: Identity2<number> = (x) => x;
三、泛型接口
3.1 基本泛型接口
接口中的泛型参数可以在整个接口中使用。
interface Box<T> {
value: T;
get(): T;
set(value: T): void;
}
const stringBox: Box<string> = {
value: "hello",
get() { return this.value; },
set(v) { this.value = v; }
};
3.2 泛型接口作为函数类型
interface Comparator<T> {
(a: T, b: T): number;
}
const numberCompare: Comparator<number> = (a, b) => a - b;
3.3 泛型接口的默认类型
interface ApiResponse<T = any> {
code: number;
data: T;
}
const resp: ApiResponse = { code: 200, data: {} }; // T 默认为 any
四、泛型约束(extends)
4.1 为什么需要约束
有时泛型函数内部需要访问某些特定属性,但并非所有类型都有该属性。约束可以限制类型必须满足某个形状。
function logLength<T>(value: T): T {
// console.log(value.length); // 错误:T 可能没有 length
return value;
}
使用 extends 约束 T 必须有 length 属性。
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(value.length);
return value;
}
logLength("hello"); // OK,string 有 length
logLength([1,2,3]); // OK,数组有 length
// logLength(42); // ❌ number 没有 length
4.2 约束为特定类型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 25 };
const name = getProperty(user, "name"); // string
// const invalid = getProperty(user, "email"); // ❌
4.3 多个泛型变量之间的约束
一个泛型参数可以约束另一个。
function copy<T extends U, U>(source: T, target: U): void {
// T 必须是 U 的子类型
}
实际场景中常用于确保两个类型具有某种关系。
4.4 约束为可构造类型(new 约束)
使用 new () => T 约束泛型参数必须有构造函数(后面会详细讲)。
function create<T>(ctor: new () => T): T {
return new ctor();
}
class User {}
const u = create(User); // User
五、泛型中的 new 与构造函数类型
5.1 构造签名
构造签名的写法:new (...args: any[]) => T 表示一个可以 new 的构造函数,返回类型 T。
interface Constructable<T> {
new (...args: any[]): T;
}
function factory<T>(ctor: Constructable<T>, ...args: any[]): T {
return new ctor(...args);
}
5.2 带参数的构造函数约束
function createInstance<T>(ctor: new (name: string) => T, name: string): T {
return new ctor(name);
}
class Animal {
constructor(public name: string) {}
}
const dog = createInstance(Animal, "Buddy"); // Animal
5.3 工厂模式中的泛型
class Factory<T> {
constructor(private ctor: new () => T) {}
produce(): T {
return new this.ctor();
}
}
class Widget {}
const widgetFactory = new Factory(Widget);
const w = widgetFactory.produce();
六、泛型参数推断与常见陷阱
6.1 类型推断规则
TypeScript 会从参数中推断类型,尽可能推断出最具体的类型。
function combine<T>(a: T, b: T): T[] {
return [a, b];
}
const result = combine(1, 2); // T 推断为 number
const result2 = combine(1, "a"); // T 推断为 (number | string),因为两个参数类型不同
6.2 不必要显式指定类型
通常依赖推断即可,显式指定反而冗余,但某些场景需要(如编译器无法推断)。
let arr: number[] = [1,2,3];
// 无法推断出 T 为 number,因为参数是普通值,但可以显式指定
function identity<T>(v: T): T { return v; }
let x = identity<number>(42);
6.3 泛型参数默认类型
可以为泛型参数指定默认类型,当未推断或未显式提供时使用默认。
function wrap<T = string>(value: T): T[] {
return [value];
}
const a = wrap(42); // T 推断为 number,忽略默认
const b = wrap(); // 错误:缺少参数,类型推断失败,且无默认可用
// 正确用法:参数也提供默认值
function wrap2<T = string>(value?: T): T[] {
return value ? [value] : [];
}
const c = wrap2(); // T 默认为 string,c 类型为 string[]
6.4 泛型约束与默认类型同时使用
interface HasLength { length: number; }
function longest<T extends HasLength = string>(a: T, b: T): T {
return a.length > b.length ? a : b;
}
// T 默认 string,但通常被推断
longest("a", "bc");
6.5 常见陷阱:泛型函数中的回调
function run<T>(fn: (arg: T) => void, arg: T): void {
fn(arg);
}
run((x: string) => console.log(x), "hello"); // OK
run((x: string) => console.log(x), 42); // ❌ 类型不匹配
另一种情况:当泛型参数在回调中推断时,可能推断为 unknown。
function forEach<T>(arr: T[], callback: (item: T) => void): void {
arr.forEach(callback);
}
forEach(["a","b"], (item) => {
console.log(item.toUpperCase()); // OK,item 为 string
});
6.6 泛型类中的静态成员陷阱
静态成员不能使用泛型参数(已在之前文章详细说明)。
七、综合示例
// 1. 泛型函数:合并对象
function merge<T extends object, U extends object>(objA: T, objB: U): T & U {
return { ...objA, ...objB };
}
const merged = merge({ name: "Alice" }, { age: 25 });
console.log(merged.name, merged.age);
// 2. 泛型接口:存储接口
interface Repository<T, ID = number | string> {
find(id: ID): T | undefined;
save(entity: T): void;
delete(id: ID): void;
}
class UserRepo implements Repository<User, number> {
private users: User[] = [];
find(id: number) { return this.users.find(u => u.id === id); }
save(u: User) { this.users.push(u); }
delete(id: number) { this.users = this.users.filter(u => u.id !== id); }
}
interface User {
id: number;
name: string;
}
// 3. 泛型约束与构造函数:实例工厂
function instantiate<T>(ctor: new (...args: any[]) => T, ...args: any[]): T {
return new ctor(...args);
}
class Product {
constructor(public name: string, public price: number) {}
}
const laptop = instantiate(Product, "Laptop", 999);
console.log(laptop.name);
// 4. 多个泛型变量 + keyof
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const users = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
const names = pluck(users, "name"); // string[]
const ids = pluck(users, "id"); // number[]
// 5. 泛型默认类型 + 可选参数
type Callback<T = void> = (err: Error | null, result?: T) => void;
function asyncTask<T>(cb: Callback<T>, value: T): void {
setTimeout(() => cb(null, value), 100);
}
asyncTask((err, result) => {
if (result) console.log(result.toUpperCase()); // result 为 string
}, "hello");
八、小结
| 概念 | 示例代码 | 说明 |
|---|---|---|
| 泛型函数 | function id<T>(x: T): T |
类型参数化 |
| 泛型接口 | interface Box<T> { value: T } |
接口中的泛型 |
| 泛型类 | class Container<T> { items: T[] } |
类级别的泛型 |
| 泛型约束 | T extends HasLength |
限制类型必须满足某结构 |
keyof 约束 |
K extends keyof T |
限制为对象的键名 |
| 构造函数类型 | new (...args: any[]) => T |
描述可构造类型 |
| 泛型默认类型 | <T = string> |
未推断时使用的默认类型 |
| 多个泛型参数 | <K, V> |
可同时使用多个 |
| 泛型与类型守卫结合 | 通常用于工具函数 | 提高代码复用性与类型安全 |
觉得文章有帮助?别忘了:
👍 点赞 👍 – 给我一点鼓励
⭐ 收藏 ⭐ – 方便以后查看
🔔 关注 🔔 – 获取更新通知
标签: #TypeScript #泛型 #泛型函数 #泛型接口 #泛型类 #学习笔记 #前端开发
更多推荐
所有评论(0)