泛型与条件类型

泛型与条件类型

写在前面

上一篇讲了联合类型与类型守卫。这一篇讲泛型——TypeScript 类型系统的灵魂。

泛型是 TS 进阶的"分水岭"——会写泛型的人写出来的工具/库强大 10 倍,不会写的人只会用别人写好的。本篇从基础到 infer 关键字,完整覆盖,最后实战实现一个类型安全的 EventEmitter。

一、泛型基础

1.1 什么是泛型

泛型是"参数化的类型"——把类型当作参数,在使用时指定。

// 没有泛型 - 每个类型写一份
function identityString(arg: string): string { return arg; }
function identityNumber(arg: number): number { return arg; }

// 用泛型 - 一个函数搞定
function identity<T>(arg: T): T { return arg; }

const a = identity("hello");  // a 是 string
const b = identity(42);       // b 是 number
const c = identity<User>({...});  // 显式指定

1.2 泛型函数

function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

first([1, 2, 3]);       // number | undefined
first(["a", "b"]);      // string | undefined
first([]);              // undefined

1.3 泛型 interface

interface Box<T> {
    value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

1.4 泛型 type

type Pair<K, V> = {
    key: K;
    value: V;
};

const pair: Pair<string, number> = { key: "age", value: 30 };

1.5 泛型类

class Container<T> {
    constructor(public value: T) {}

    map<U>(fn: (value: T) => U): Container<U> {
        return new Container(fn(this.value));
    }
}

const c = new Container(42);
const s = c.map(n => n.toString());  // Container<string>

二、泛型约束

2.1 基础约束

// 错误 - T 没有 length 属性
function logLength<T>(arg: T): void {
    console.log(arg.length);  // 报错
}

// 正确 - 用 extends 约束 T 至少有 length
function logLength<T extends { length: number }>(arg: T): void {
    console.log(arg.length);
}

logLength("hello");     // OK
logLength([1, 2, 3]);  // OK
logLength({ length: 5 });  // OK
logLength(42);          // 报错 - number 没有 length

2.2 keyof 约束

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };
getProperty(user, "name");   // string
getProperty(user, "age");    // number
getProperty(user, "foo");    // 报错 - 'foo' 不是 user 的 key

T[K] 是索引访问类型——返回该 key 对应的值类型。

2.3 条件约束

// T 必须是对象
function merge<T extends object, U extends object>(a: T, b: U): T & U {
    return { ...a, ...b };
}

// T 必须是 string | number | boolean
function process<T extends string | number | boolean>(value: T): T {
    return value;
}

2.4 默认类型参数

interface ApiResponse<T = unknown, E = Error> {
    data?: T;
    error?: E;
}

const r1: ApiResponse = { data: { ... } };  // T 是 unknown
const r2: ApiResponse<User> = { data: { ... } };  // T 是 User
const r3: ApiResponse<User, NetworkError> = { ... };  // 都指定

三、条件类型

3.1 基础语法

T extends U ? X : Y

T 是 U 的子类型吗?是的话是 X,不是的话是 Y。

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<"hello">;  // true(字面量是 string 的子类型)

3.2 条件类型的实际意义

条件类型让类型可以"分支"——根据输入类型决定输出类型。

// 把数组转成元素类型,其他保持不变
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<number[]>;     // number
type B = Flatten<string>;       // string
type C = Flatten<boolean[]>;    // boolean

infer U 表示"推断出数组元素类型"——见下一节详解。

3.3 实战:提取 Promise 内部类型

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;   // string
type B = UnwrapPromise<Promise<User>>;     // User
type C = UnwrapPromise<number>;            // number

这个模式叫 Awaited<T>——TypeScript 内置了,效果一样。

四、infer 关键字

4.1 什么是 infer

infer 在条件类型的 extends 子句中,声明一个"待推断"的类型变量。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function foo(): number { return 42; }

type FooReturn = ReturnType<typeof foo>;  // number

TypeScript 推断出函数返回类型 R,作为条件类型的结果。

4.2 实战:提取函数参数类型

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function greet(name: string, age: number): void {}

type GreetParams = Parameters<typeof greet>;  // [name: string, age: number]

// 取第一个参数
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type GreetFirst = FirstParam<typeof greet>;  // string

内置 Parameters<T> 跟这个一样。

4.3 实战:提取数组元素类型

type ElementOf<T> = T extends (infer E)[] ? E : T;

type A = ElementOf<string[]>;     // string
type B = ElementOf<number[]>;     // number
type C = ElementOf<boolean>;      // boolean

内置 Array<T> 反过来用即可:T[number]。

4.4 实战:实现类型安全的 EventEmitter

type EventMap = {
    "user:login": { userId: string; timestamp: number };
    "user:logout": { userId: string };
    "cart:add": { productId: string; quantity: number };
    "error": Error;
};

class TypedEmitter<E extends Record<string, any>> {
    private listeners: { [K in keyof E]?: Array<(data: E[K]) => void> } = {};

    on<K extends keyof E>(event: K, listener: (data: E[K]) => void): void {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event]!.push(listener);
    }

    off<K extends keyof E>(event: K, listener: (data: E[K]) => void): void {
        const arr = this.listeners[event];
        if (arr) {
            this.listeners[event] = arr.filter(l => l !== listener);
        }
    }

    emit<K extends keyof E>(event: K, data: E[K]): void {
        this.listeners[event]?.forEach(l => l(data));
    }
}

// 用法
const emitter = new TypedEmitter<EventMap>();

emitter.on("user:login", (data) => {
    // data 是 { userId: string; timestamp: number }
    console.log(data.userId, data.timestamp);
});

emitter.emit("user:login", { userId: "123", timestamp: Date.now() });
emitter.emit("user:login", { userId: 123 });  // 报错!userId 应该是 string
emitter.emit("foo", {});                      // 报错!"foo" 不是合法事件

每个事件的参数类型都精确——这就是类型安全。

五、分布式条件类型

5.1 什么是分布式条件类型

当泛型是裸类型参数(裸 T,没有包装),且是联合类型时,条件类型会自动分布到每个成员。

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>;
// 不是 (string | number)[]
// 而是 string[] | number[]

// 等价于:
type A = string[] | number[];

这是"分布"——条件类型对联合类型的每个成员分别求值。

5.2 用方括号禁止分布

// 禁止分布 - 整个联合类型作为一个整体
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type A = ToArrayNonDist<string | number>;
// 是 (string | number)[]

5.3 实战:Exclude 和 Extract

// 内置 Exclude - 排除 T 中是 U 的子类型的成员
type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<"a" | "b" | "c", "a" | "b">;  // "c"

// 内置 Extract - 提取 T 中是 U 的子类型的成员
type Extract<T, U> = T extends U ? T : never;

type B = Extract<"a" | "b" | "c", "a" | "b">;  // "a" | "b"

这两个都依赖分布式条件类型——没有分布就没法用。

5.4 实战:NonNullable

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | null | undefined>;  // string
type B = NonNullable<number | null>;              // number

5.5 实战:filter 工具

// 提取 T 中有特定属性的成员
type WithProperty<T, K extends string> = T extends { [P in K]: any } ? T : never;

interface User { type: "user"; name: string }
interface Admin { type: "admin"; name: string; permissions: string[] }
interface Product { type: "product"; sku: string }

type WithName = WithProperty<User | Admin | Product, "name">;  // User | Admin
type WithPermissions = WithProperty<User | Admin | Product, "permissions">;  // Admin

六、映射类型(Mapped Types)

6.1 基础映射

type Readonly<T> = {
    readonly [K in keyof T]: T[K];
};

type Optional<T> = {
    [K in keyof T]?: T[K];
};

interface User { name: string; age: number; }
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }

6.2 映射修饰符

// 添加/删除 readonly
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

// 添加/删除 optional
type Required<T> = {
    [K in keyof T]-?: T[K];
};

+ 和 - 修饰符——+ 加,- 减。

6.3 实战:类型安全的 API client

// 全部转成可选
type PartialUpdate<T> = Partial<T>;

// 全部转成 nullable
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

// 全部转成 promise
type Promisify<T> = {
    [K in keyof T]: Promise<T[K]>;
};

6.4 实战:从 enum-like 联合类型生成映射

type Status = "pending" | "success" | "error";

// 从联合类型生成对象类型
type StatusMap = {
    [K in Status]: { value: K; label: string };
};
// {
//   pending: { value: "pending"; label: string };
//   success: { value: "success"; label: string };
//   error: { value: "error"; label: string };
// }

七、内置工具类型

7.1 常用工具

// 对象相关
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = { [P in K]: T };

// 联合类型
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T extends null | undefined ? never : T;

// 函数
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Awaited<T> = T extends Promise<infer U> ? U : T;

// 字符串
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;

7.2 Pick 和 Omit 实战

interface User {
    id: string;
    name: string;
    email: string;
    password: string;
    createdAt: Date;
}

// 只需要 name 和 email
type PublicUser = Pick<User, "id" | "name" | "email">;

// 不要 password
type SafeUser = Omit<User, "password">;

7.3 Record 实战

// 枚举映射
const statusLabels: Record<"pending" | "success" | "error", string> = {
    pending: "处理中",
    success: "成功",
    error: "失败",
};

八、类型体操的几个常见套路

8.1 套路 1:从函数提取参数

type FirstParameter<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

function fn(name: string, age: number, active: boolean) {}
type First = FirstParameter<typeof fn>;  // string

8.2 套路 2:从数组提取第一个

type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

type A = First<[1, 2, 3]>;  // 1
type B = First<[]>;          // never

8.3 套路 3:从对象提取特定 key 的值类型

type ValueOf<T, K extends keyof T> = T[K];

interface User { name: string; age: number; }
type Age = ValueOf<User, "age">;  // number

8.4 套路 4:深度 Readonly

type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
    server: { host: string; port: number };
    database: { url: string };
}

type Frozen = DeepReadonly<Config>;
// {
//   readonly server: { readonly host: string; readonly port: number };
//   readonly database: { readonly url: string };
// }

8.5 套路 5:对象转联合

type ObjectToUnion<T> = T[keyof T];

interface User { name: string; age: number; }
type Values = ObjectToUnion<User>;  // string | number

8.6 套路 6:字符串转联合

type Split<S extends string, Sep extends string = ""> =
    S extends `${infer First}${Sep}${infer Rest}`
        ? First | Split<Rest, Sep>
        : S;

type Path = Split<"users.posts.comments", ".">;
// "users" | "posts" | "comments"

九、实战:类型安全的 API 客户端

9.1 定义 API 端点

type ApiEndpoints = {
    "/users": {
        GET: { response: User[]; query?: { limit?: number } };
        POST: { body: CreateUserInput; response: User };
    };
    "/users/:id": {
        GET: { params: { id: string }; response: User };
        DELETE: { params: { id: string }; response: void };
    };
    "/orders": {
        GET: { response: Order[] };
        POST: { body: CreateOrderInput; response: Order };
    };
};

9.2 类型安全的请求函数

type Method = keyof ApiEndpoints["/users"];  // "GET" | "POST"
type PathParams<T extends string> =
    T extends `${string}:${infer P}/${infer R}` ? P | PathParams<R> :
    T extends `${string}:${infer P}` ? P :
    never;

async function request<
    P extends keyof ApiEndpoints,
    M extends keyof ApiEndpoints[P]
>(
    method: M,
    path: P,
    options: ApiEndpoints[P][M] extends { body: infer B } ? { body: B } :
             ApiEndpoints[P][M] extends { params: infer P } ? { params: P } :
             ApiEndpoints[P][M] extends { query: infer Q } ? { query: Q } :
             {} = {} as any
): Promise<ApiEndpoints[P][M] extends { response: infer R } ? R : void> {
    // 实际请求逻辑
    return fetch(...).then(r => r.json());
}

// 用法
const users = await request("GET", "/users");
// users 是 User[]

const user = await request("GET", "/users/:id", { params: { id: "123" } });
// user 是 User

十、本篇小结

概念 关键点
泛型 参数化的类型
约束 T extends X
条件类型 T extends U ? X : Y
infer 在条件类型中推断子类型
分布式 裸 T 对联合类型自动分布
映射类型 批量转换对象属性
实战 应用
EventEmitter 事件名 + payload 类型精确
API 客户端 端点 + 方法 + 参数类型精确
状态机 不同状态不同 payload
工具库 类型安全的 helper

下一篇讲 模板字面量类型——TS 4.1+ 的字符串类型魔法,把路由路径提取成类型。

参考资料