TypeScript 常用泛型工具函数

目录

什么是泛型工具类型?

[1. 属性修饰符:让类型"变个身"](#1. 属性修饰符:让类型“变个身”)

[Partial ------ 变"全员可选"](#Partial —— 变“全员可选”)

[Required ------ 变"全员必选"](#Required —— 变“全员必选”)

[Readonly ------ 变"只读"](#Readonly —— 变“只读”)

[2. 结构过滤:像筛子一样筛选类型](#2. 结构过滤:像筛子一样筛选类型)

[Pick ------ 只取我要的](#Pick —— 只取我要的)

[Omit ------ 排除我不要的](#Omit —— 排除我不要的)

[3. 函数类型侦探:提取函数的秘密](#3. 函数类型侦探:提取函数的秘密)

[Parameters ------ 提取参数类型](#Parameters —— 提取参数类型)

[ReturnType ------ 提取返回值类型](#ReturnType —— 提取返回值类型)

[4. 映射与构造:快速创建结构](#4. 映射与构造:快速创建结构)

[Record ------ 键值对工厂](#Record —— 键值对工厂)

[5. 进阶实战:手写自定义工具](#5. 进阶实战:手写自定义工具)

[实战 1:DeepPartial (深度可选)](#实战 1:DeepPartial (深度可选))

[实战 2:Get (点符号访问类型)](#实战 2:Get (点符号访问类型))

参考官方文档:

什么是泛型工具类型?

简单来说,泛型工具类型就是用来操作类型的函数

普通函数是 Input -> Output(如 map 把数组转换成新数组)。

泛型工具类型是 Type -> Type(如 Partial 把所有属性变成可选)。

TypeScript 内置了大量开箱即用的工具类型,覆盖了前端开发 90% 的场景。

1. 属性修饰符:让类型"变个身"

这是最基础也是最高频的一类工具,主要用于修改对象属性的读写性或必选性。

Partial ------ 变"全员可选"

场景:当你有一个"编辑用户信息"的表单,用户可能只想修改昵称,不想改头像。你不需要把所有字段都重写一遍。

javascript 复制代码
interface User {
  id: number;
  name: string;
  age: number;
  avatar: string;
}

// 场景:定义更新接口,不需要传 id,且其他字段都可选
type UserUpdateForm = Partial<Omit<User, 'id'>>;

// 等价于:
// type UserUpdateForm = {
//   name?: string;
//   age?: number;
//   avatar?: string;
// }

function updateUser(id: number, data: UserUpdateForm) {
  // ...
}

Required ------ 变"全员必选"

场景Partial 的反操作。比如从后端拿来的配置对象可能有缺省,但在运行时你确保它已经补全了,可以用它断言类型。

Readonly ------ 变"只读"

场景:Redux 的 State 或者 Vue/React 的 Props 定义,防止在组件内部意外修改父级传递的数据。

javascript 复制代码
type ReadonlyState = Readonly<{
  count: number;
}>;
// state.count = 2; // ❌ Error: Cannot assign to 'count' because it is read-only

2. 结构过滤:像筛子一样筛选类型

如果你只想从大接口中拿几个字段,或者剔除几个敏感字段,这一类工具是你的救星。

Pick<T, K> ------ 只取我要的

场景 :后端返回了一个包含 20 个字段的"用户详情",但你只需要渲染一个"用户卡片",只需要 id, name, avatar

javascript 复制代码
interface UserDetail {
  id: number;
  name: string;
  email: string;
  phone: string;
  passwordHash: string; // 敏感信息
  lastLoginIp: string;
}

// 只需要这三个字段做卡片展示
type UserCardProps = Pick<UserDetail, 'id' | 'name' | 'avatar'>;

Omit<T, K> ------ 排除我不要的

场景 :创建文章列表,数据结构和创建文章表单几乎一样,但列表不需要 content 字段(太长了),且不需要 id(新建时没有)。

javascript 复制代码
interface Article {
  id: number;
  title: string;
  content: string;
  tags: string[];
}

// 新建文章表单:不需要 id
type CreateArticleDto = Omit<Article, 'id'>;

小技巧Omit 其实可以用 PickExclude 组合实现,但 TS 内置了它,直接用更爽。

3. 函数类型侦探:提取函数的秘密

在编写高阶组件或装饰器时,我们经常需要知道一个函数"接收什么参数"以及"返回什么类型"。

Parameters ------ 提取参数类型

场景:你要写一个日志装饰器,包裹任意函数,打印它的参数名和值。

javascript 复制代码
function log<T extends (...args: any[]) => any>(fn: T) {
  return function(this: any, ...args: Parameters<T>) {
    console.log('Function called with:', args);
    return fn.apply(this, args);
  };
}

function add(a: number, b: number) {
  return a + b;
}

const wrappedAdd = log(add);
// wrappedAdd 的参数类型自动推断为

ReturnType ------ 提取返回值类型

场景:异步请求封装时,你想基于 API 函数的返回值定义 Redux 的 Action 类型。

javascript 复制代码
async function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return { name: 'Jack', age: 18 };
}

// 自动推导 fetchUser 返回的 Promise 内部结构
type UserType = Awaited<ReturnType<typeof fetchUser>>; 
// UserType = { name: string; age: number }

4. 映射与构造:快速创建结构

Record<Keys, Type> ------ 键值对工厂

场景:定义一个枚举对象,或者一个以 ID 为 Key 的字典。

javascript 复制代码
// 定义一个角色权限映射
type Role = 'admin' | 'user' | 'guest';

type Permission = {
  read: boolean;
  write: boolean;
};

// 快速生成对象结构:所有角色的权限列表
type RolePermissions = Record<Role, Permission>;

const permissions: RolePermissions = {
  admin: { read: true, write: true },
  user:  { read: true, write: false },
  guest: { read: false, write: false },
};

5. 进阶实战:手写自定义工具

内置的很好用,但有时候我们需要更强大的功能。通过泛型递归和条件类型,我们可以自己造"轮子"。

实战 1:DeepPartial (深度可选)

内置的 Partial 只能处理一层。如果对象是嵌套的,我们需要递归地把所有层级的属性都变为可选。

javascript 复制代码
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
  server: {
    host: string;
    port: number;
  };
  db: {
    name: string;
  };
}

// 现在嵌套属性也是可选的了
type PartialConfig = DeepPartial<Config>;
// partialConfig.server?.host // 合法

实战 2:Get (点符号访问类型)

类似 Lodash 的 _.get,但是是在类型层面操作。根据字符串路径获取深层属性的类型。

javascript 复制代码
type Get<T, P> = P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? Get<T[K], Rest>
    : never
  : P extends keyof T
  ? T[P]
  : never;

interface ApiData {
  user: {
    info: {
      name: string;
    };
  };
}

// 根据路径 'user.info.name' 获取类型 string
type UserNameType = Get<ApiData, 'user.info.name'>; // string

参考官方文档:

TypeScript Utility Types 官方文档