Frontend · #typescript#types#generics

TypeScript高级类型编程

2025.05.21 TypeScript 7 min 2.8k
// 目录 · contents

引言

TypeScript 的类型系统是图灵完备的,这意味着你可以在类型层面进行复杂的逻辑运算。掌握高级类型编程不仅能让你写出更安全的代码,还能构建出优雅的 API 设计。本文将系统性地介绍泛型约束、条件类型、映射类型、模板字面量类型、infer 关键字,以及标准工具类型的底层实现。

泛型与约束

基础泛型

泛型允许你编写适用于多种类型的代码,同时保持类型安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Basic generic function
function identity<T>(value: T): T {
return value;
}

const num = identity(42); // number
const str = identity('hello'); // string

// Generic interface
interface Container<T> {
value: T;
map<U>(fn: (val: T) => U): Container<U>;
}

泛型约束(extends)

通过 extends 关键字限制泛型参数必须满足特定条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Constrain T to have a length property
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}

getLength('hello'); // OK: string has length
getLength([1, 2, 3]); // OK: array has length
// getLength(42); // Error: number doesn't have length

// Constrain with keyof
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 age = getProperty(user, 'age'); // number
// getProperty(user, 'email'); // Error: 'email' is not in keyof typeof user

多重约束与默认类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Multiple constraints using intersection
interface Serializable {
serialize(): string;
}

interface Loggable {
log(): void;
}

function process<T extends Serializable & Loggable>(item: T): string {
item.log();
return item.serialize();
}

// Default generic types
interface ApiResponse<T = unknown, E = Error> {
data: T;
error: E | null;
status: number;
}

// Uses defaults
const response: ApiResponse = { data: {}, error: null, status: 200 };
// Override defaults
const typedResponse: ApiResponse<User[], ApiError> = { /* ... */ } as any;

条件类型

条件类型是 TypeScript 类型编程的核心,语法类似三元表达式:

1
2
3
4
5
// Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

分布式条件类型

当条件类型作用于联合类型时,会自动分布到联合的每个成员:

1
2
3
4
5
6
7
8
9
10
11
type ToArray<T> = T extends any ? T[] : never;

// Distributes over union
type Result = ToArray<string | number>;
// = (string extends any ? string[] : never) | (number extends any ? number[] : never)
// = string[] | number[]

// Prevent distribution with tuple wrapping
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDistributive<string | number>;
// = (string | number)[]
graph TD
    A["ToArray&lt;string | number&gt;"] --> B[分布式展开]
    B --> C["ToArray&lt;string&gt;"]
    B --> D["ToArray&lt;number&gt;"]
    C --> E["string[]"]
    D --> F["number[]"]
    E --> G["string[] | number[]"]
    F --> G
    style A fill:#3178c6,color:#fff
    style G fill:#2ecc71,color:#fff

实用条件类型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Exclude null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type Cleaned = NonNullable<string | null | undefined>; // string

// Extract from union
type Extract<T, U> = T extends U ? T : never;
type Strings = Extract<string | number | boolean, string | boolean>;
// string | boolean

// Filter function overloads
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

interface Mixed {
name: string;
age: number;
greet(): void;
save(): Promise<void>;
}

type FnProps = FunctionPropertyNames<Mixed>; // 'greet' | 'save'

infer 关键字

infer 在条件类型中声明一个待推断的类型变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Infer return type
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => Promise<boolean>>; // Promise<boolean>

// Infer parameter types
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type P1 = MyParameters<(a: string, b: number) => void>; // [string, number]

// Infer array element type
type ElementType<T> = T extends (infer E)[] ? E : T;

type E1 = ElementType<string[]>; // string
type E2 = ElementType<number>; // number (fallback)

// Infer Promise value
type Awaited<T> = T extends Promise<infer U>
? U extends Promise<any>
? Awaited<U> // Recursive unwrap
: U
: T;

type A1 = Awaited<Promise<string>>; // string
type A2 = Awaited<Promise<Promise<number>>>; // number

infer 的高级用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Infer first and rest of tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;

type F = First<[1, 2, 3]>; // 1
type R = Rest<[1, 2, 3]>; // [2, 3]

// Infer string patterns
type ParseRoute<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ParseRoute<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;

type Params = ParseRoute<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

映射类型

映射类型基于已有类型创建新类型,遍历键集合进行转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Basic mapped type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};

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

type Required<T> = {
[K in keyof T]-?: T[K]; // -? removes optionality
};

// Key remapping with `as`
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
name: string;
age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Filter keys during mapping
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};

type WithoutFunctions = OmitByType<Mixed, Function>;
// { name: string; age: number; }
graph LR
    A[Source Type T] --> B["映射 [K in keyof T]"]
    B --> C{Key Remapping?}
    C -->|as clause| D[转换 key 名称]
    C -->|无| E[保持原 key]
    D --> F[Value Transform]
    E --> F
    F --> G[New Mapped Type]

    style A fill:#3178c6,color:#fff
    style B fill:#9b59b6,color:#fff
    style G fill:#2ecc71,color:#fff

模板字面量类型

TypeScript 4.1 引入的模板字面量类型将字符串字面量类型与模板字符串语法结合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type EventName = 'click' | 'scroll' | 'mousemove';
type EventHandler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onScroll' | 'onMousemove'

// Intrinsic string manipulation types
type Upper = Uppercase<'hello'>; // 'HELLO'
type Lower = Lowercase<'HELLO'>; // 'hello'
type Cap = Capitalize<'hello'>; // 'Hello'
type Uncap = Uncapitalize<'Hello'>; // 'hello'

// Cross product of unions in templates
type Color = 'red' | 'blue';
type Size = 'sm' | 'md' | 'lg';
type ClassName = `${Color}-${Size}`;
// 'red-sm' | 'red-md' | 'red-lg' | 'blue-sm' | 'blue-md' | 'blue-lg'

// Type-safe CSS units
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

function setWidth(width: CSSValue) { /* ... */ }
setWidth('100px'); // OK
setWidth('2.5rem'); // OK
// setWidth('100'); // Error

递归模板字面量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Type-safe dot-notation path
type PathOf<T, Prefix extends string = ''> = {
[K in keyof T & string]: T[K] extends object
? `${Prefix}${K}` | PathOf<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string];

interface Config {
db: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
cache: {
ttl: number;
};
}

type ConfigPath = PathOf<Config>;
// 'db' | 'db.host' | 'db.port' | 'db.credentials' | 'db.credentials.user'
// | 'db.credentials.password' | 'cache' | 'cache.ttl'

工具类型实现

理解标准工具类型的实现有助于掌握类型编程的核心技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Pick: select specific keys
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};

// Omit: exclude specific keys
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};

// Record: create object type from keys and value type
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};

// DeepPartial: recursively make all properties optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

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

// Merge two types (second overrides first)
type Merge<A, B> = Omit<A, keyof B> & B;

interface Base {
id: number;
name: string;
active: boolean;
}

type Override = { name: string | null; role: string };
type Merged = Merge<Base, Override>;
// { id: number; active: boolean; name: string | null; role: string }

类型体操实战

实现 TupleToUnion

1
2
3
4
type TupleToUnion<T extends any[]> = T[number];

type Union = TupleToUnion<[string, number, boolean]>;
// string | number | boolean

实现 Flatten

1
2
3
4
5
6
7
8
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
? First extends any[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: [];

type Flat = Flatten<[1, [2, [3, 4]], [5]]>;
// [1, 2, 3, 4, 5]

实现类型安全的事件发射器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type EventMap = {
login: { userId: string; timestamp: number };
logout: { userId: string };
error: { code: number; message: string };
};

class TypedEventEmitter<T extends Record<string, any>> {
private handlers = new Map<string, Set<Function>>();

on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
if (!this.handlers.has(event as string)) {
this.handlers.set(event as string, new Set());
}
this.handlers.get(event as string)!.add(handler);
}

emit<K extends keyof T>(event: K, payload: T[K]): void {
this.handlers.get(event as string)?.forEach(h => h(payload));
}

off<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
this.handlers.get(event as string)?.delete(handler);
}
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on('login', (data) => {
// data is { userId: string; timestamp: number } — fully typed!
console.log(data.userId, data.timestamp);
});

emitter.emit('login', { userId: '123', timestamp: Date.now() }); // OK
// emitter.emit('login', { userId: '123' }); // Error: missing timestamp

类型编程思维导图

graph TD
    A[TypeScript 高级类型] --> B[泛型 Generics]
    A --> C[条件类型 Conditional]
    A --> D[映射类型 Mapped]
    A --> E[模板字面量 Template Literal]

    B --> B1[约束 extends]
    B --> B2[默认类型参数]
    B --> B3[泛型推断]

    C --> C1[分布式条件类型]
    C --> C2[infer 推断]
    C --> C3[递归条件类型]

    D --> D1[Key Remapping as]
    D --> D2[修饰符 +/- readonly/?]
    D --> D3[过滤键]

    E --> E1[字符串操作类型]
    E --> E2[联合类型叉积]
    E --> E3[模式匹配]

    style A fill:#3178c6,color:#fff
    style B fill:#e74c3c,color:#fff
    style C fill:#f39c12,color:#000
    style D fill:#2ecc71,color:#fff
    style E fill:#9b59b6,color:#fff

总结

TypeScript 的类型系统远不止 interfacetype 别名。通过泛型约束、条件类型、infer 推断、映射类型和模板字面量类型的组合使用,可以构建出极其精确和灵活的类型定义。建议在实际项目中循序渐进地应用这些技巧:从简单的泛型函数开始,逐步过渡到条件类型和映射类型,最终能够设计出类型安全的 API。记住,类型编程的目的是为开发者服务——如果类型过于复杂导致可读性下降,适当简化也是明智的选择。

作者 · authorzt
发布 · date2025-05-21
篇幅 · length2.8k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论