强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

TypeScript 开发指南 / 10 - 工具类型

工具类型

TypeScript 内置了一系列泛型工具类型(Utility Types),用于常见的类型变换。

Partial<T>

将类型 T 的所有属性变为可选:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 所有属性都变成可选
type PartialUser = Partial<User>;
// 等价于:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// 使用场景:更新函数(只更新传入的字段)
function updateUser(id: number, updates: Partial<User>): void {
  // 只更新传入的字段
}

updateUser(1, { name: "Bob" });          // ✅ 只更新 name
updateUser(1, { name: "Bob", age: 30 }); // ✅ 更新多个字段

实现原理

// Partial 的实现
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

Required<T>

将类型 T 的所有属性变为必选:

interface Config {
  host?: string;
  port?: number;
  ssl?: boolean;
}

// 所有属性变成必选
type RequiredConfig = Required<Config>;
// {
//   host: string;
//   port: number;
//   ssl: boolean;
// }

const config: RequiredConfig = {
  host: "localhost",
  port: 3000,
  ssl: false
};

实现原理

type MyRequired<T> = {
  [P in keyof T]-?: T[P];  // -? 移除可选修饰符
};

Readonly<T>

将类型 T 的所有属性变为只读:

interface State {
  count: number;
  items: string[];
}

type ReadonlyState = Readonly<State>;

const state: ReadonlyState = { count: 0, items: [] };
state.count = 1;  // ❌ 错误:只读属性
state.items = []; // ❌ 错误

实现原理

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

Pick<T, K>

从类型 T 中选取指定的属性 K

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

// 只选取 id 和 name
type UserBasic = Pick<User, "id" | "name">;
// {
//   id: number;
//   name: string;
// }

// 使用场景:API 响应只返回部分字段
function getUserBasic(id: number): UserBasic {
  return { id, name: "Alice" };
}

实现原理

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Omit<T, K>

从类型 T 中排除指定的属性 K

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 排除 password
type SafeUser = Omit<User, "password">;
// {
//   id: number;
//   name: string;
//   email: string;
// }

// 使用场景:API 响应排除敏感字段
function getSafeUser(user: User): SafeUser {
  const { password, ...rest } = user;
  return rest;
}

实现原理

type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Record<K, T>

构造一个键类型为 K、值类型为 T 的对象类型:

// 基本用法
type UserMap = Record<string, User>;

const users: UserMap = {
  "user-1": { id: 1, name: "Alice", email: "a@b.com", password: "..." },
  "user-2": { id: 2, name: "Bob", email: "b@c.com", password: "..." }
};

// 字面量键
type PageInfo = Record<"home" | "about" | "contact", { title: string; url: string }>;

const pages: PageInfo = {
  home: { title: "首页", url: "/" },
  about: { title: "关于", url: "/about" },
  contact: { title: "联系", url: "/contact" }
};

实现原理

type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

Exclude<T, U>

从联合类型 T 中排除可以赋值给 U 的类型:

type Status = "active" | "inactive" | "banned" | "deleted";

// 排除 "banned" 和 "deleted"
type ActiveStatus = Exclude<Status, "banned" | "deleted">;
// "active" | "inactive"

// 排除特定类型
type StringOrNumber = string | number | boolean | null;
type NotNull = Exclude<StringOrNumber, null>;
// string | number | boolean

实现原理

type MyExclude<T, U> = T extends U ? never : T;

Extract<T, U>

从联合类型 T 中提取可以赋值给 U 的类型:

type Status = "active" | "inactive" | "banned" | "deleted";

// 提取包含 "ed" 的类型
type DeletedStatus = Extract<Status, `${string}ed`>;
// "banned" | "deleted"

// 提取特定类型
type StringOrNumber = string | number | boolean;
type OnlyString = Extract<StringOrNumber, string>;
// string

实现原理

type MyExtract<T, U> = T extends U ? T : never;

NonNullable<T>

从类型 T 中排除 nullundefined

type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>;
// string

// 使用场景
function process(value: NonNullable<string | null>): void {
  // value: string
  console.log(value.toUpperCase());
}

实现原理

type MyNonNullable<T> = T & {};
// 或者
type MyNonNullable2<T> = T extends null | undefined ? never : T;

ReturnType<T>

获取函数类型 T 的返回值类型:

function getUser() {
  return { id: 1, name: "Alice", email: "alice@example.com" };
}

type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }

// 异步函数
async function fetchData() {
  const response = await fetch("/api/data");
  return response.json();
}

type Data = ReturnType<typeof fetchData>; // Promise<any>

实现原理

type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

Parameters<T>

获取函数类型 T 的参数类型元组:

function createUser(name: string, age: number, email: string): User {
  return { id: 1, name, age, email, password: "" };
}

type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number, email: string]

// 使用场景:包装函数
function withLogging<T extends (...args: any[]) => any>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => {
    console.log("调用参数:", args);
    return fn(...args);
  };
}

const loggedCreateUser = withLogging(createUser);

ConstructorParameters<T>

获取构造函数的参数类型:

class User {
  constructor(
    public name: string,
    public age: number
  ) {}
}

type UserConstructorParams = ConstructorParameters<typeof User>;
// [name: string, age: number]

// 使用场景:工厂函数
function createInstance<T extends new (...args: any[]) => any>(
  ctor: T,
  ...args: ConstructorParameters<T>
): InstanceType<T> {
  return new ctor(...args);
}

const user = createInstance(User, "Alice", 25);

InstanceType<T>

获取构造函数的实例类型:

class User {
  name: string = "";
  greet() { return "Hello"; }
}

type UserInstance = InstanceType<typeof User>;
// User

// 工厂模式
type Constructor<T = {}> = new (...args: any[]) => T;

function createFactory<T>(ctor: Constructor<T>): () => T {
  return () => new ctor();
}

Awaited<T>

递归解包 Promise 类型(TypeScript 4.5+):

type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<number>>>;  // number
type C = Awaited<boolean | Promise<number>>; // boolean | number

// 使用场景
async function getData(): Promise<string> {
  return "hello";
}

type Data = Awaited<ReturnType<typeof getData>>; // string

组合使用工具类型

interface FullUser {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
  createdAt: Date;
  updatedAt: Date;
}

// 创建用户(排除 id 和时间戳)
type CreateUserData = Omit<FullUser, "id" | "createdAt" | "updatedAt">;

// 更新用户(所有字段可选,排除 id)
type UpdateUserData = Omit<Partial<FullUser>, "id">;

// 用户列表项(只包含基本信息)
type UserListItem = Pick<FullUser, "id" | "name" | "email" | "role">;

// API 响应(排除密码)
type SafeUser = Omit<FullUser, "password">;
type UserResponse = {
  data: SafeUser;
  status: number;
};

自定义工具类型

// DeepPartial - 递归可选
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
  };
}

// 所有嵌套属性都可选
type PartialConfig = DeepPartial<Config>;

const config: PartialConfig = {
  database: {
    host: "localhost"
    // 其他字段可选
  }
};

// DeepReadonly - 递归只读
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Mutable - 移除 readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// RequiredKeys - 获取所有必选属性的键
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

// OptionalKeys - 获取所有可选属性的键
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

业务场景:表单类型

interface FormFields {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
  bio: string;
}

// 表单状态
type FormState = {
  values: FormFields;
  errors: Partial<Record<keyof FormFields, string[]>>;
  touched: Partial<Record<keyof FormFields, boolean>>;
  isSubmitting: boolean;
};

// 表单变更
type FormChangeEvent = {
  field: keyof FormFields;
  value: FormFields[keyof FormFields];
};

// 表单验证规则
type ValidationRules = {
  [K in keyof FormFields]?: {
    required?: boolean;
    minLength?: number;
    maxLength?: number;
    pattern?: RegExp;
    custom?: (value: FormFields[K]) => string | null;
  };
};

注意事项

  1. 工具类型只在编译时存在——运行时没有工具类型的概念
  2. Partial 适用于更新操作——只传入需要修改的字段
  3. Omit 比 Pick 更常用——通常排除敏感字段比选取字段更常见
  4. 组合使用工具类型可以实现复杂的类型变换
  5. 自定义工具类型可以放在 types/ 目录下统一管理

扩展阅读