本文献给:

已掌握 TypeScript 基础类型、接口、类、函数类型等知识的开发者。本文将系统讲解 TypeScript 泛型的核心概念,包括泛型函数、泛型接口、泛型类、泛型约束、构造函数类型以及泛型参数推断,帮助你写出更灵活、更安全的可复用代码。


你将学到:

  1. 为什么需要泛型 —— 类型参数化的价值
  2. 泛型函数的基本写法与类型推断
  3. 泛型接口的定义与使用
  4. 泛型约束(extends)与多个泛型变量
  5. 泛型中的 new 与构造函数类型
  6. 泛型参数推断规则与常见陷阱
  7. 泛型默认类型



一、为什么需要泛型

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 #泛型 #泛型函数 #泛型接口 #泛型类 #学习笔记 #前端开发

更多推荐