keyof、typeof、索引访问与标准工具类型

假设用户对象已经有一份类型定义,表单、更新接口和字段选择器还要各抄一份吗?keyof、索引访问和工具类型能从已有定义推导出新类型。先理解推导前后的关系,再记各个工具的名字,会比背一张语法表更容易。

这些操作符处理的是类型,不是实际对象。例如,得到一组属性名的类型,并不等于执行了 Object.keys。把类型推导和运行时代码分开看,才能知道什么时候只是减少重复声明,什么时候还需要真正的函数。

1. keyof

TypeScript
type User = { id: string age: number } type UserKey = keyof User // "id" | "age" function read<T extends object, K extends keyof T>(target: T, key: K): T[K] { return target[key] }

若类型具有字符串索引签名,keyof 可能包含 string | number,因为 JavaScript 对象数值键会转为字符串。

2. 类型位置的 typeof

TypeScript
const defaults = { retry: 3, mode: "safe", } as const type Defaults = typeof defaults type Mode = Defaults["mode"] // "safe"

类型 typeof 查询一个值的静态类型;运行时 typeof value 返回字符串标签,二者语法相同但发生阶段不同。

3. 索引访问类型

TypeScript
type ApiResponse = { data: Array<{ id: string; title: string }> } type Row = ApiResponse["data"][number] type RowId = Row["id"]

T[number] 取得数组/元组所有元素类型的联合。

4. 常用工具类型分类

属性变换

TypeScript
type DraftUser = Partial<User> type CompleteUser = Required<DraftUser> type PublicUser = Readonly<Pick<User, "id" | "age">> type WithoutAge = Omit<User, "age"> type UserMap = Record<string, User>

联合过滤

TypeScript
type Value = string | number | null | undefined type Present = NonNullable<Value> // string | number type Text = Extract<Value, string> // string type NonText = Exclude<Value, string> // number | null | undefined

函数与构造器

TypeScript
function load(id: string, refresh = false) { return Promise.resolve({ id, refresh }) } type LoadArgs = Parameters<typeof load> type LoadPromise = ReturnType<typeof load> type Loaded = Awaited<LoadPromise>

ReturnType 对重载函数只保留最后一个签名的推导视角,不能自动得到所有重载返回联合。

5. NoInfer

当某参数应被检查但不应参与类型参数推断时,可用 NoInfer<T>:

TypeScript
function choose<C extends string>( options: readonly [C, ...C[]], defaultValue?: NoInfer<C>, ): C { return defaultValue ?? options[0] } choose(["red", "yellow", "green"] as const, "red") // choose([...], "blue") // blue 不应扩宽 C

这里要求非空元组,才有资格承诺返回 C。任意数组加 options[0]! 会在空数组时返回 undefined,非空断言不会修复它。若选项来自外部数据,仍需先在运行时验证非空;NoInfer 只控制推断来源,不验证数组长度。

6. 工具类型是浅层的

Readonly<User> 只把第一层属性标成只读;Partial<User> 也只让第一层属性可选。需要递归版本时必须定义清楚数组、函数、集合和内建对象的行为,避免一个万能 DeepPartial 在不同领域产生错误语义。

7. 从值生成类型,而非重复抄写

TypeScript
const roles = ["reader", "editor", "admin"] as const type Role = (typeof roles)[number] function isRole(value: string): value is Role { return (roles as readonly string[]).includes(value) }

这里运行时白名单和类型来自同一来源,减少漂移。若只写 type Role = ...,运行时仍需要另一份数据。

8. 可读性边界

导出的公共类型应有业务名称。把五层工具类型直接写在函数参数上会让错误信息和文档难读;可先定义中间别名,并为类型级逻辑写编译期断言测试。

参考:Keyof、Typeof、Indexed Access、Utility Types。