Skip to content

工具类型深入


核心问题解析

问题一: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 响应等场景中极其常用。


学习目标详解

目标一:深入理解 PartialRequiredReadonlyPickOmit

工具类型作用实现
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;
};

📌 实用场景

  1. 固定键的配置对象
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' },
};
  1. 计数器或状态映射
ts
type Status = 'idle' | 'loading' | 'success' | 'error';
const statusCount: Record<Status, number> = {
  idle: 0, loading: 0, success: 0, error: 0
};

目标三:使用 ExcludeExtractNonNullable 进行类型过滤

这些工具类型基于条件类型,用于操作联合类型。

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>:排除 nullundefined
ts
type T3 = NonNullable<string | number | null | undefined>; // string | number

实现:

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

🔍 这些类型在处理 Promise<T>、可选属性、联合类型时非常有用。


目标四:理解 ParametersReturnTypeConstructorParameters 的实现原理

这些工具类型用于函数类型解析

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

不使用 PickExclude,直接实现 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 在参数位置推断。