keyof、索引访问与映射类型

本节目标

// 运行环境:Node.js 16+ + typescript;类型检查 npx tsc --noEmit ts-l11.ts

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

// keyof:把「所有键」变成一个联合类型
type UserKeys = keyof User // 'id' | 'name' | 'age'

// 索引访问类型 T[K]:取「K 这个键对应的值类型」
type UserId = User['id']   // number
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key] // 返回值类型精确跟随 key
}
const u: User = { id: 1, name: 'Tom', age: 18 }
const idVal = getField(u, 'id')   // 推断为 number
const nameVal = getField(u, 'name') // 推断为 string

// 映射类型:遍历键,批量改造
type ReadonlyUser = {
  readonly [K in keyof User]: User[K]
}
// 等价于把所有字段都加 readonly
const ru: ReadonlyUser = { id: 1, name: 'Tom', age: 18 }
// ru.id = 2 // ❌ readonly

[K in keyof T] 读作「对 T 的每个键 K」,这就是「映射类型」——像 for 循环一样遍历键并生成新结构。Partial<T>、Required<T>、Readonly<T> 这些内置工具类型,底层全是映射类型。

名词解释

keyof:「取键运算符」。keyof User 得到 'id' | 'name' | 'age' 这样的「键的联合类型」。它让你可以「在类型层面谈论对象的属性名」,是做通用工具类型的基石。

映射类型(Mapped Type):用 [K in keyof T] 语法「遍历一个类型的所有键,对每个键做同样改造」并生成新类型。类比:它是类型层面的 for...of。TS 内置的 Partial/Readonly/Record 都是它的产物——你写的映射类型能复用这种「批量改造」能力。

课后练习

练习 1:用映射类型写一个 Nullable<T>,把 T 每个字段都变成 原类型 | null。

答案:

type Nullable<T> = { [K in keyof T]: T[K] | null }

练习 2:User['name' | 'age'] 是什么类型?

答案:string | number。索引访问类型支持「传入键的联合」,结果就是「对应值类型的联合」。

总结

keyof + 索引访问 + 映射类型,合起来就是 TS 的「反射三件套」。我的观点:这是从「会用 TS」跃迁到「会造 TS 工具」的关键一跃。一旦你理解「键也能当成类型来运算」,很多重复的类型定义就能写成一个通用映射类型——比如全项目要加 readonly、Partial、或把所有字段改成可选,一行映射类型搞定,改一处全局生效。新手常卡在「[K in keyof T] 到底在干嘛」,我的记忆法是:把 K 想象成循环变量,T[K] 想象成「当前这个字段的类型」,整个结构就是「对每个字段做同样的事」。