工具类型深入
核心问题解析
问题一:Partial<T> 是怎么实现的?
我们经常用 Partial<User> 表示“User 的所有属性都是可选的”。
ts
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// 等价于:
// {
// id?: number;
// name?: string;
// email?: string;
// }✅ 实现原理:使用映射类型和可选修饰符 ?:
ts
type MyPartial<T> = {
[K in keyof T]?: T[K];
};keyof T:获取T的所有键[K in ...]:遍历每个键?:将每个属性变为可选
问题二:如何让某些属性必选或只读?
有时我们需要反向操作:
- 把可选属性变必选 →
Required<T> - 把属性变只读 →
Readonly<T>
✅ Required<T> 实现:
ts
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // `-?` 移除可选性
};✅ Readonly<T> 实现:
ts
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};也可以部分只读:
ts
type ReadonlyId<T> = {
readonly id: T['id'];
} & Omit<T, 'id'>;问题三:怎么从类型中提取一部分?
我们希望像 SQL 的 SELECT 一样,从类型中“查询”子集。
✅ 使用 Pick<T, K> 提取指定属性:
ts
type UserInfo = Pick<User, 'id' | 'name'>;
// 结果:{ id: number; name: string; }✅ 使用 Omit<T, K> 排除某些属性:
ts
type UserWithoutEmail = Omit<User, 'email'>;
// 结果:{ id: number; name: string; }🔍 这在表单、DTO、API 响应等场景中极其常用。
学习目标详解
目标一:深入理解 Partial、Required、Readonly、Pick、Omit
| 工具类型 | 作用 | 实现 |
|---|---|---|
Partial<T> | 所有属性可选 | { [K in keyof T]?: T[K] } |
Required<T> | 所有属性必选 | { [K in keyof T]-?: T[K] } |
Readonly<T> | 所有属性只读 | { readonly [K in keyof T]: T[K] } |
Pick<T, K> | 提取部分属性 | { [P in K]: T[P] } |
Omit<T, K> | 排除部分属性 | Pick<T, Exclude<keyof T, K>> |
📌 Omit 的巧妙实现:
ts
type MyOmit<T, K> = Pick<T, Exclude<keyof T, K>>;Exclude<keyof T, K>:从所有键中排除K- 再用
Pick提取剩余键
目标二:掌握 Record<K, T> 构建键值映射
当你需要一个“字典”或“映射表”时,Record 是最佳选择。
ts
const scores: Record<string, number> = {
Alice: 95,
Bob: 87,
};✅ 定义:
ts
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
};📌 实用场景:
- 固定键的配置对象:
ts
type Environment = 'development' | 'production' | 'test';
type Config = Record<Environment, { apiUrl: string }>;
const configs: Config = {
development: { apiUrl: 'http://dev.api.com' },
production: { apiUrl: 'https://api.com' },
test: { apiUrl: 'http://test.api.com' },
};- 计数器或状态映射:
ts
type Status = 'idle' | 'loading' | 'success' | 'error';
const statusCount: Record<Status, number> = {
idle: 0, loading: 0, success: 0, error: 0
};目标三:使用 Exclude、Extract、NonNullable 进行类型过滤
这些工具类型基于条件类型,用于操作联合类型。
1. Exclude<T, U>:从 T 中排除 U
ts
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'实现:
ts
type MyExclude<T, U> = T extends U ? never : T;2. Extract<T, U>:提取 T 中属于 U 的部分
ts
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'实现:
ts
type MyExtract<T, U> = T extends U ? T : never;3. NonNullable<T>:排除 null 和 undefined
ts
type T3 = NonNullable<string | number | null | undefined>; // string | number实现:
ts
type MyNonNullable<T> = T extends null | undefined ? never : T;🔍 这些类型在处理
Promise<T>、可选属性、联合类型时非常有用。
目标四:理解 Parameters、ReturnType、ConstructorParameters 的实现原理
这些工具类型用于函数类型解析。
1. ReturnType<T>:提取函数返回值类型
ts
function getUser() {
return { id: 1, name: 'Alice' };
}
type User = ReturnType<typeof getUser>; // { id: number; name: string; }实现:
ts
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;infer R:声明一个待推断的类型变量R- 如果
T是函数,返回其返回值类型R
2. Parameters<T>:提取函数参数类型
ts
type Args = Parameters<typeof getUser>; // [](无参数)实现:
ts
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;返回一个元组类型,表示所有参数。
3. ConstructorParameters<T>:提取构造函数参数
ts
class User {
constructor(public id: number, public name: string) {}
}
type CtorArgs = ConstructorParameters<typeof User>; // [number, string]实现类似 Parameters,但要求 T 是构造函数类型。
总结:本节核心要点
| 工具类型 | 用途 | 关键技术 |
|---|---|---|
Partial / Required | 可选性控制 | 映射类型 ? / -? |
Readonly | 只读控制 | readonly 修饰符 |
Pick / Omit | 属性提取/排除 | keyof + 映射 |
Record<K, T> | 键值对映射 | 固定键的字典 |
Exclude / Extract | 联合类型过滤 | 条件类型 + 分布式 |
ReturnType / Parameters | 函数类型解析 | infer 类型推断 |
📌 掌握这些工具类型,你就能像搭积木一样组合出任意复杂的类型结构。
练习题
练习题 1:手写 MyOmit
不使用 Pick 和 Exclude,直接实现 MyOmit<T, K>:
ts
type MyOmit<T, K> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
// 测试
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyOmit<Todo, 'description'>;
// 应等价于 { title: string; completed: boolean; }提示:使用
as进行键重映射。
练习题 2:实现 PickByType
实现一个工具类型,根据属性值的类型来提取属性:
ts
type PickByType<T, Value> = {
[K in keyof T as T[K] extends Value ? K : never]: T[K]
};
interface Example {
name: string;
age: number;
isActive: boolean;
score: number;
}
type NumberProps = PickByType<Example, number>;
// { age: number; score: number; }提示:结合
as条件键映射。
练习题 3:Record 实战
使用 Record 定义一个颜色主题映射,支持以下结构:
ts
const theme = {
light: { primary: '#007bff', secondary: '#6c757d' },
dark: { primary: '#0056b3', secondary: '#495057' }
};
type ThemeName = 'light' | 'dark';
type ColorPalette = { primary: string; secondary: string };
type Theme = Record<ThemeName, ColorPalette>;验证 theme 是否符合 Theme 类型。
练习题 4:条件类型过滤
分析以下类型的结果:
ts
type T0 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
type T1 = Extract<'a' | 'b' | 'c', string>; // 'a' | 'b' | 'c'
type T2 = Extract<'a' | 'b' | 'c', 'd'>; // never
type T3 = NonNullable<string \| undefined>; // string练习题 5:infer 深入
实现一个 FirstArg<F> 工具类型,提取函数的第一个参数类型:
ts
type FirstArg<F> = F extends (arg: infer T, ...args: any[]) => any ? T : never;
type T4 = FirstArg<(name: string) => void>; // string
type T5 = FirstArg<(id: number, name: string) => void>; // number
type T6 = FirstArg<() => void>; // never提示:使用
infer在参数位置推断。