【TS】day11-utility-types

作者:mario 发布时间: 2026-08-28 阅读量:7 评论数:0

TypeScript 内置工具类型 — 官方预制件全家桶,用一遍就回不去了

前三天你学会了造类型工厂(泛型、约束、泛型类)。今天参观官方预制件仓库:Partial、Pick、Omit、Record、Exclude、ReturnType……17 个工具类型,每一个都是"泛型 + 约束 + 类型运算"的组合成品。学完今天,你写 CRUD 接口类型从手写 5 个 interface 变成派生 5 行——而且实体加字段时全部自动同步。


目录


一、工具类型是什么

1.1 定义

工具类型(Utility Type) = 接收类型作为参数、返回新类型的"类型函数"。它们全用泛型实现,是官方提炼的最高频类型变换:

// 你已经在前几天反复用过它们(可能没意识到):
type Patch = Partial<Device>;              // D10 Repository.update 的参数
type Rule = Record<DeviceKind, Config>;    // D5 状态配置表
type Ret = ReturnType<typeof fn>;          // D6 函数反解

1.2 学习方法(重要)

// ❌ 死记 17 个名词
// ✅ 按家族理解 + 每个类型亲手写一遍"使用场景"

// 今天每个工具类型都遵循这个结构:
// 1. 签名(它长什么样)
// 2. 效果(输入 → 输出对照)
// 3. 场景(什么业务用它)
// 4. 原理预告(Day 12/13 手写它)

1.3 演示用基础类型

// 本文档统一示例实体(贯穿全文)
interface Device {
  id: string;
  name: string;
  temp: number;
  status: "running" | "standby" | "fault";
  createdAt: number;
}

type AnyStatus = "running" | "standby" | "fault" | "offline";

二、结构变换家族

2.1 Partial<T>:全部变可选

type DevicePatch = Partial<Device>;
// {
//   id?: string;
//   name?: string;
//   temp?: number;
//   status?: "running" | "standby" | "fault";
//   createdAt?: number;
// }

// ===== 场景 1:更新接口(PATCH 语义)=====
function updateDevice(id: string, patch: Partial<Device>): void {
  // 只传要改的字段
}
updateDevice("CNC-001", { temp: 88 });        // ✅ 只改温度
updateDevice("CNC-001", { name: "新机床" });   // ✅ 只改名字

// ===== 场景 2:表单草稿(边填边存)=====
const draft: Partial<Device> = { name: "机床" };   // 还没填完
draft.temp = 65;                                    // 逐步补全

// ===== 场景 3:配置合并(默认配置 + 用户覆盖)=====
const DEFAULT_CONFIG = { url: "ws://x", retry: 3, timeout: 5000 };
function createConfig(userCfg: Partial<typeof DEFAULT_CONFIG>) {
  return { ...DEFAULT_CONFIG, ...userCfg };   // 浅合并
}
createConfig({ retry: 5 });   // ✅ 只覆盖 retry

2.2 Required<T>:全部变必填

// Partial 的反向操作
type StrictDevice = Required<Partial<Device>>;
// 还原成 Device(全必填)

// ===== 场景:强制补全可选结构 =====
interface Options {
  width?: number;
  height?: number;
}

// 填充默认值后,后续流程需要"必有值"的保证
function withDefaults(o: Options): Required<Options> {
  return { width: o.width ?? 1920, height: o.height ?? 1080 };
}

const opts = withDefaults({});
opts.width.toFixed(0);    // ✅ number(不再是 number | undefined)

2.3 Readonly<T>:全部变只读

type FrozenDevice = Readonly<Device>;
// {
//   readonly id: string;
//   readonly name: string;
//   ...
// }

const snapshot: FrozenDevice = { id: "1", name: "机床", temp: 65, status: "running", createdAt: 0 };
snapshot.temp = 90;   // ❌ 编译错误:只读

// ===== 场景 1:数据快照(防止误改历史记录)=====
function takeSnapshot(devices: Device[]): readonly Device[] {
  return devices.map(d => Object.freeze(d));
}

// ===== 场景 2:配置对象冻结 =====
const THEME: Readonly<{ bg: string; accent: string }> = {
  bg: "#0a1628",
  accent: "#00ff88"
};
// THEME.bg = "red";   // ❌ 编译期拦截

2.4 Pick<T, K>:挑字段

type DeviceBrief = Pick<Device, "id" | "temp">;
// { id: string; temp: number }

// ===== 场景 1:列表 DTO(瘦身传输)=====
// 大屏列表页只显示编号和温度 —— 拒绝多余字段防误用
function getBriefs(devices: Device[]): Pick<Device, "id" | "temp">[] {
  return devices.map(({ id, temp }) => ({ id, temp }));
}

// ===== 场景 2:权限视图(隐藏敏感字段)=====
type PublicUser = Pick<User, "id" | "name">;    // 不含 password
function toPublic(user: User): PublicUser {
  return { id: user.id, name: user.name };
}

// ===== 场景 3:精确解构的类型标注 =====
const brief: Pick<Device, "id" | "status"> = { id: "1", status: "running" };

2.5 Omit<T, K>:删字段(Pick 的反向)

type DeviceCreate = Omit<Device, "id" | "createdAt">;
// { name: string; temp: number; status: "running" | "standby" | "fault" }

// ===== 场景 1:创建表单(id 由后端生成)=====
function createDevice(body: DeviceCreate): Promise<Device> { /* ... */ }
createDevice({ name: "新机床", temp: 40, status: "standby" });   // ✅
createDevice({ id: "x", name: "n", temp: 1, status: "running", createdAt: 0 });  // ❌ 多传了

// ===== 场景 2:更新表单(不允许改 id/创建时间)=====
type DeviceUpdate = Partial<Omit<Device, "id" | "createdAt">>;
// 全可选 + 剔除不可改字段 —— PATCH 的完整类型

// ⚠ 注意 Omit 的 K 不受约束(拼错不报错):
type Bad = Omit<Device, "idd">;     // ✅ 通过!等于没删
// 严格版(Day 13 手写):StrictOmit<T, K extends keyof T>

三、键值映射家族

3.1 Record<K, V>:完整映射表

// ===== 基本用法(第 5 天已实战,今天系统化)=====

// 键是联合 → 每个键都必须有值(完整性!)
const STATUS_CONFIG: Record<AnyStatus, { text: string; color: string }> = {
  running: { text: "运行中", color: "#00ff88" },
  standby: { text: "待机",   color: "#ffaa00" },
  fault:   { text: "故障",   color: "#ff4444" },
  offline: { text: "离线",   color: "#7a8ba0" }
};
// 删掉任何一行 → 编译错误(完整性是 Record 的核心价值)

// ===== 键也可以是字面量联合的派生 =====
type Metric = "temp" | "speed" | "pressure";
const UNITS: Record<Metric, string> = {
  temp: "°C",
  speed: "rpm",
  pressure: "kPa"
};

// ===== 值也可以是复杂类型 =====
type Handler = (payload: unknown) => void;
const HANDLERS: Record<string, Handler> = {};

// ===== 键为 string 的"宽"用法(字典模式)=====
const index: Record<string, Device> = {};
index["CNC-001"] = device;

3.2 Record 的等价手写

// Record<K, V> 本质是映射类型(Day 13 手写):
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

// keyof any = string | number | symbol(所有可能的键类型)
// K extends keyof any:约束 K 为"可作键的东西"

3.3 Record + keyof typeof:配置表三连(终极形态)

// 三步走(第 5 天的模式,用 Record 的视角再看一遍):

// 第 1 步:写常量(真相源)
const DEVICE_STATUS = {
  running: { text: "运行中", color: "#00ff88", level: 0 },
  standby: { text: "待机",   color: "#ffaa00", level: 1 },
  fault:   { text: "故障",   color: "#ff4444", level: 2 }
} as const;

// 第 2 步:派生键类型
type DeviceStatus = keyof typeof DEVICE_STATUS;   // "running" | "standby" | "fault"

// 第 3 步:Record 保证任何"补充映射表"的完整性
const STATUS_ICON: Record<DeviceStatus, string> = {
  running: "🟢",
  standby: "🟡",
  fault:   "🔴"
};
// DEVICE_STATUS 加新状态 → STATUS_ICON 立刻报缺 → 编译器替你查漏 ⭐

四、联合运算家族

4.1 Exclude<T, U>:从联合中排除

type Active = "running" | "standby";
type Inactive = Exclude<AnyStatus, Active>;
// "fault" | "offline"(排除掉 running 和 standby)

// ===== 场景 1:状态分类 =====
function isCritical(s: AnyStatus): s is Inactive {
  return s === "fault" || s === "offline";   // 收窄目标用派生联合(D4 谓词)
}

// ===== 场景 2:Omit 的实现原理(预告 Day 13)=====
// Omit<T, K> = Pick<T, Exclude<keyof T, K>>
// keyof T 排除 K → 剩下的键 → Pick 回来

// ===== 场景 3:过滤可写字段 =====
type AllKeys = keyof Device;   // "id" | "name" | "temp" | "status" | "createdAt"
type EditableKeys = Exclude<AllKeys, "id" | "createdAt">;
// "name" | "temp" | "status"
type EditableFields = Pick<Device, EditableKeys>;
// 等价于 Omit<Device, "id" | "createdAt"> —— 两种写法一回事

4.2 Extract<T, U>:从联合中提取(交集)

type Common = Extract<AnyStatus, Active>;
// "running" | "standby"(只保留两边都有的)

// ===== 场景:接口能力的交集 =====
type BackendStatus = "running" | "standby" | "fault" | "maintain";
type FrontendSupported = Extract<BackendStatus, AnyStatus>;
// "running" | "standby" | "fault" —— maintain 前端还没做,编译期就隔离

4.3 NonNullable<T>:去空

type MaybeTemp = number | null | undefined;
type Temp = NonNullable<MaybeTemp>;    // number

// ===== 场景 1:回调链的收窄固化 =====
function findDevice(id: string): Device | undefined { /* ... */ }

// 把"已检查过非空"的结果存起来
const found = findDevice("CNC-001");
if (found) {
  const confirmed: NonNullable<typeof found> = found;   // Device
  // 后续使用 confirmed 不再带 undefined
}

// ===== 场景 2:清洗数组类型(配合 filter)=====
const list: (Device | null | undefined)[] = [device, null, undefined];
const cleaned = list.filter((d): d is Device => d != null);
// cleaned: Device[](D4 谓词版)
const cleaned2 = list.filter(d => d != null) as NonNullable<typeof list[number]>[];
// as 版(不推荐,谓词更优雅)

五、函数反解家族

5.1 ReturnType<T>:返回值类型

function fetchDevices(): Promise<Device[]> { /* ... */ }
function createAlert(): AlertRecord { /* ... */ }

type R1 = ReturnType<typeof fetchDevices>;   // Promise<Device[]>
type R2 = ReturnType<typeof createAlert>;    // AlertRecord

// ===== 场景 1:不导出接口也能引用返回值类型 =====
// API 函数没导出类型?从函数反解(第 6 天知识,工具化)
const result = await fetchDevices();
type Devices = Awaited<ReturnType<typeof fetchDevices>>;   // Device[]

// ===== 场景 2:Vue3 store 反解(真实高频)=====
function useDeviceStore() {
  const devices = ref<Device[]>([]);
  const keyword = ref("");
  function load() { /* ... */ }
  return { devices, keyword, load };
}
type DeviceStore = ReturnType<typeof useDeviceStore>;
// { devices: Ref<Device[]>; keyword: Ref<string>; load: () => void }

5.2 Awaited<T>:解 Promise

type A1 = Awaited<Promise<Device>>;        // Device
type A2 = Awaited<Promise<Device[]>>;      // Device[]
type A3 = Awaited<Device>;                 // Device(非 Promise 原样返回)
type A4 = Awaited<Promise<Promise<Device>>>;  // Device(递归解包!)

// ===== 场景:async 函数的数据类型 =====
async function loadStats(): Promise<DeviceStats> { /* ... */ }
type Stats = Awaited<ReturnType<typeof loadStats>>;   // DeviceStats
// 这是"异步接口数据类型"的标准写法,背下来

5.3 Parameters<T>:参数元组

function createDevice(id: string, temp: number, status?: DeviceStatus) { /* ... */ }

type Args = Parameters<typeof createDevice>;
// [id: string, temp: number, status?: DeviceStatus](元组!)

type FirstArg = Args[0];      // string(D6 元组索引!)
type SecondArg = Args[1];     // number

// ===== 场景 1:转发参数 =====
function logAndCreate(...args: Parameters<typeof createDevice>) {
  console.log("创建设备", args[0]);
  return createDevice(...args);    // 展开透传
}

// ===== 场景 2:debounce 的参数保持(D9 伏笔全解)=====
function debounce<F extends (...args: any[]) => any>(
  fn: F,
  delay: number
): (...args: Parameters<F>) => void {
  let timer: number | undefined;
  return (...args: Parameters<F>) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay) as unknown as number;
  };
}
// Parameters<F> 把原函数的参数元组"抄"给包装函数 —— 参数类型零丢失

5.4 InstanceType<T>:类的实例类型

class DataHub {
  devices: Device[] = [];
  refresh(): void {}
}

type Hub = InstanceType<typeof DataHub>;   // DataHub(实例类型)

// ⚠ typeof DataHub 是"构造函数类型",InstanceType 才是实例
// 对比:
type Ctor = typeof DataHub;         // new () => DataHub
type Inst = InstanceType<typeof DataHub>;   // DataHub

// ===== 场景:不 import 类也能标注实例 =====
// 工厂函数返回实例,外部用 InstanceType 反解(避免循环依赖时常用)
function createHub(): InstanceType<typeof DataHub> {
  return new DataHub();
}

六、其他实用工具

6.1 ThisType<T>(了解)

// 指定对象字面量方法里 this 的类型(Vue2 选项式 API 的类型原理)
interface Store {
  state: { count: number };
  increment(this: Store): void;   // 显式 this 标注(更常用的方式)
}

6.2 特殊位置的 Partial/Required 组合技

// ===== 嵌套 Partial 不生效(重要认知)=====
interface Config {
  server: { host: string; port: number };
  client: { url: string; timeout: number };
}

type P1 = Partial<Config>;
// { server?: { host: string; port: number }; client?: {...} }
// ⚠ 只把第一层变可选 —— server 存在但 host 还是必填!

// 想全层可选 → DeepPartial(Day 12/14 自定义):
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

type P2 = DeepPartial<Config>;
// { server?: { host?: string; port?: number }; ... } ✅ 全层可选

七、速查总表(打印贴墙版)

工具类型

一句话

典型场景

Partial<T>

全可选

PATCH 更新、表单草稿、配置合并

Required<T>

全必填

默认值填充后

Readonly<T>

全只读

快照、配置冻结

Pick<T, K>

挑字段

列表 DTO、权限视图

Omit<T, K>

删字段

创建表单(去 id)⚠ 键不受约束

Record<K, V>

键值全映射

配置表、状态表(完整性保证)

Exclude<T, U>

联合排除

字面量过滤、Omit 原理

Extract<T, U>

联合提取

能力交集

NonNullable<T>

去空

收窄固化

Parameters<T>

参数元组

函数包装、参数透传

ReturnType<T>

返回值

函数反解、store 类型

Awaited<T>

解 Promise

async 数据类型

InstanceType<T>

实例类型

类反解

ThisType<T>

this 类型

对象方法上下文(了解)

家族记忆法

结构四兄弟:Partial(松)/ Required(紧)/ Readonly(锁)/ Pick-Omit(裁)
映射一尊佛:Record(表)
联合两兄妹:Exclude(删)/ Extract(留)
反解三剑客:Parameters(进)/ ReturnType(出)/ Awaited(拆快递)

八、实战场景全覆盖

8.1 场景一:完整 CRUD 接口的类型派生(今日主菜)

// ===== 实体(唯一的真相源)=====
interface Device {
  id: string;
  name: string;
  temp: number;
  status: "running" | "standby" | "fault";
  createdAt: number;
  updatedAt: number;
}

// ===== 五个接口类型全部派生(零手写重复)=====

/** 创建:不要 id/时间戳(后端生成) */
type DeviceCreate = Omit<Device, "id" | "createdAt" | "updatedAt">;

/** 更新:部分字段 + 不许改 id/时间 */
type DeviceUpdate = Partial<Omit<Device, "id" | "createdAt" | "updatedAt">>;

/** 列表查询:筛选条件 + 分页 */
type DeviceQuery = Partial<Pick<Device, "status">> & {
  page?: number;
  pageSize?: number;
  keyword?: string;
};

/** 列表项(瘦身 DTO)*/
type DeviceListItem = Pick<Device, "id" | "name" | "temp" | "status">;

/** 详情响应 */
type DeviceDetail = Readonly<Device>;    // 详情页只展示,标记只读

// ===== API 层签名(每个函数类型精确)=====
interface DeviceApi {
  create(body: DeviceCreate): Promise<Device>;
  update(id: string, body: DeviceUpdate): Promise<Device>;
  remove(id: string): Promise<void>;
  list(query: DeviceQuery): Promise<Page<DeviceListItem>>;
  detail(id: string): Promise<DeviceDetail | null>;
}

// ===== 收益验证 =====
// Device 加字段 voltage: number:
//   Create/Update 自动包含 voltage ✅
//   ListItem 不变(白名单制)✅
//   手写 5 个 interface 的时代:改 5 处,漏 1 处就是 bug ⚠

8.2 场景二:分页 + 工具类型的组合

/** 通用分页响应 */
interface Page<T> {
  list: T[];
  total: number;
  page: number;
  pageSize: number;
}

/** 查询参数(所有列表页通用的骨架) */
type BaseQuery<T, K extends keyof T> = {
  page?: number;
  pageSize?: number;
  orderBy?: K;
  order?: "asc" | "desc";
};

type DeviceSortQuery = BaseQuery<Device, keyof Device>;
// orderBy 可以是任何 Device 字段 —— 按不存在字段排序直接爆红

8.3 场景三:状态机的类型编排

// ===== 状态机:状态 + 事件 + 转移表 全类型化 =====
type Status = "idle" | "loading" | "success" | "error";

/** 事件按状态限定(idle 只能开始,loading 只能结束)*/
type EventOf<S extends Status> =
  S extends "idle" ? "START"
  : S extends "loading" ? "SUCCESS" | "FAIL"
  : S extends "error" ? "RETRY"
  : never;   // success 是终态,没有事件(条件类型 Day 12 预告)

/** 转移表:Record 的完整性保证状态机无死角 */
const TRANSITIONS: Record<Status, Partial<Record<EventOf<Status>, Status>>> = {
  idle:    { START: "loading" },
  loading: { SUCCESS: "success", FAIL: "error" },
  error:   { RETRY: "loading" },
  success: {}    // 终态
};
// 状态机改状态/事件 → 转移表漏配编译爆红 —— 状态机 bug 的编译期防线

8.4 场景四:Vue3 组件 Props 的派生(超前预演)

// 第 29 周将写 Vue3 组件,Props 类型同样用工具类型派生:

/** 图表组件的 Props:从配置类型裁剪 */
interface ChartConfig {
  option: EChartsOption;
  height: number;
  autoResize: boolean;
  theme: "dark" | "light";
}

// 组件 Props:height 和 autoResize 允许外部不传(有默认值),其余必传
type ChartProps = Omit<Partial<ChartConfig>, "option"> & {
  option: ChartConfig["option"];    // option 必传
};

// 手写等价物对比(体会派生的省心):
// {
//   option: EChartsOption;
//   height?: number;
//   autoResize?: boolean;
//   theme?: "dark" | "light";
// }

九、类比记忆:预制件仓库

把类型系统想成建筑工地:

工具类型

类比

说明

手写 interface

现场浇筑

灵活但重复劳动多

Partial<T>

可调节支架

松一点(全部可缺)

Required<T>

拧紧所有螺栓

紧回去

Readonly<T>

浇筑完成挂牌

固化,禁止触碰

Pick<T, K>

按图纸挑梁柱

白名单

Omit<T, K>

敲掉不要的墙

黑名单

Record<K, V>

标准孔位模板

每个孔都必须拧螺丝(完整性)

Exclude/Extract

钢筋筛选机

联合里的进/出

ReturnType<T>

看机器出料口

反推产出

Parameters<T>

进料口

反推原料

Awaited<T>

拆传送带

拿到最终成品

官方工具 = 预制件

工厂预制、工地组装

又快又标准

心法:能组装就不现浇——派生类型永远和实体同步。


十、常见坑点与最佳实践

坑点 1:Partial 只浅层生效

// ❌ 期望嵌套可选,实际只第一层
type P = Partial<Config>;   // server?: { host: string } ← host 仍必填

// ✅ 深层可选用 DeepPartial(自定义,Day 14 手写)

坑点 2:Omit 的键拼错不报错

type Bad = Omit<Device, "ids">;    // ✅ 静默通过,等于没删!

// ✅ 严格版自定义:
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Good = StrictOmit<Device, "ids">;   // ❌ 编译错误

坑点 3:Record 的键是宽 string 时失去完整性

// ❌ 键宽化:Record 不再保证"每个状态都有配置"
const M: Record<string, Config> = { running: cfg };   // ✅ 通过但只有一项

// ✅ 键用字面量联合才有完整性保证
const M2: Record<AnyStatus, Config> = { running: cfg };   // ❌ 缺三个键

坑点 4:Readonly 是浅只读

interface Config { server: { host: string } }
const c: Readonly<Config> = { server: { host: "x" } };
c.server = { host: "y" };    // ❌ 第一层拦住
c.server.host = "y";         // ✅ 第二层放行了!

// 深只读 → DeepReadonly(Day 14 手写)
// 运行时硬保证 → Object.freeze(递归版)

坑点 5:工具类型堆叠的阅读性崩塌

// ❌ 一行套五层,review 时没人看得懂
type X = Partial<Readonly<Pick<Omit<Device, "a">, "b" | "c">>>;

// ✅ 拆中间类型 + 注释
type DeviceEditable = Omit<Device, "id" | "createdAt">;  // 可编辑字段
type DeviceEditView = Pick<DeviceEditable, "name" | "temp">;  // 编辑表单可见
type DraftView = Partial<DeviceEditView>;   // 草稿(填一半)

坑点 6:ReturnType 用于重载函数

// 重载函数的 ReturnType 只取【最后一个】签名
function f(x: string): string;
function f(x: number): number;
function f(x: any): any { return x; }

type R = ReturnType<typeof f>;   // number(只看最后一个重载)
// 多态反解失真 —— 这种场景手动导出类型更稳

坑点 7:Awaited 拆的不是"你想的那层"

type A = Awaited<Promise<Promise<Device>>>;   // Device(递归全拆)
// 如果业务需要"只拆一层"—— 自定义:
type UnwrapOne<T> = T extends Promise<infer V> ? V : T;

最佳实践清单

  1. 接口类型优先派生:Create/Update/Query/DTO 全部从实体派生

  2. Record 做配置表,键必须是字面量联合(保完整性)

  3. Omit 换成 StrictOmit(自定义)防拼写静默失败

  4. 堆叠不超过两层,拆中间类型命名

  5. 深层结构用 Deep 系列(DeepPartial/DeepReadonly,Day 14 入库)

  6. 函数反解三剑客背熟:Awaited<ReturnType<typeof fn>> 是 async 数据类型标准式

  7. 不要为派生而派生:只用一次的 { id: string } 直接写更清楚


十一、自测挑战

Q1:PATCH 更新接口的 body 类型用什么工具类型?和创建接口的有什么区别?

Q2Partial<Config> 对嵌套对象 Config 生效几层?

Q3Pick<Device, "id" | "temp"> 和手写 { id: string; temp: number } 谁更好?什么时候必须用前者?

Q4Omit<Device, "ids">(拼错)会发生什么?怎么防?

Q5:Record 的完整性保证在什么前提下失效?

Q6Exclude<AnyStatus, "fault" | "offline"> 的结果?写出计算过程。

Q7Awaited<ReturnType<typeof loadAsync>> 中的两个工具各干什么?

Q8Parameters<F> 返回的是什么形态的类型?怎么取第一个参数的类型?

Q9typeof DataHubInstanceType<typeof DataHub> 的区别?

Q10:实体 Device 加字段 voltage 后,下列类型哪些自动包含它:
DeviceCreate(Omit 去掉 id)、DeviceUpdate(Partial)、DeviceListItem(Pick 白名单)、STATUS_CONFIG(Record<DeviceStatus,…>)?

Q11:手写 NonNull<T> = Exclude<T, null | undefined>,验证与 NonNullable 等价。

Q12:设计"权限字段视图":User 含 password,写 PublicUser 类型并说明用了什么工具。


十二、总结与知识图谱

内置工具类型(官方预制件)
│
├── 结构变换
│   ├── Partial<T>(全可选)→ PATCH/草稿/配置合并
│   ├── Required<T>(全必填)→ 默认值填充后
│   ├── Readonly<T>(全只读)→ 快照/冻结 ⚠ 浅层
│   ├── Pick<T, K>(白名单)→ DTO/权限视图
│   └── Omit<T, K>(黑名单)→ 创建表单 ⚠ 键不受约束
│
├── 键值映射
│   └── Record<K, V>(完整性表)→ 配置/状态机
│       └── 前提:键必须是字面量联合
│
├── 联合运算
│   ├── Exclude<T, U>(删)→ Omit 的原理
│   ├── Extract<T, U>(留)→ 能力交集
│   └── NonNullable<T>(去空)→ 收窄固化
│
├── 函数反解
│   ├── Parameters<T>(参数元组)→ 透传/包装
│   ├── ReturnType<T>(返回值)→ store/API 反解
│   ├── Awaited<T>(解 Promise)→ 递归拆包
│   └── InstanceType<T>(实例)→ 类反解
│
├── 组合范式
│   ├── CRUD 五类型全派生(Create/Update/Query/DTO/Detail)
│   ├── Record + keyof typeof(配置表三连)
│   └── ⚠ 深层需 Deep 系列(自定义预告)
│
└── 使用纪律
    ├── 堆叠 ≤ 2 层,拆中间类型
    ├── StrictOmit 防拼写静默失败
    └── 一次性类型直接写,不为派生而派生

一句话总结:工具类型是官方用泛型造好的预制件——结构四兄弟调松紧、Record 做完整表、反解三剑客从函数抠类型;接口类型全部从实体派生,加字段时处处自动同步。


延伸阅读

资源

说明

TS Handbook - Utility Types

官方 17 个工具类型文档(今日权威出处)

TypeScript Playground

悬停验证每个派生结果


下一步

本文是 TypeScript 深入 系列的第 11 天。接下来两天拆开预制件看内部构造:

  • 第 12 天:条件类型与 infer — 类型层三元运算(Exclude/ReturnType 的源码级原理)

  • 第 13 天:映射类型 — 手写 Partial/Pick/Omit(今天用的,明天造)


学编程就像蜗牛往上爬,慢一点没关系,关键是不停下来。

每天花 2 小时,28 天通关 TypeScript 深入。加油!


评论