先记住这个答案
keyof T 得到 T 在类型层可使用的属性键集合,K extends keyof T 限制本次键参数只能从中选择。返回类型 T[K] 再把选中的键映射到对应字段值,使读取名称得到字符串、读取标识得到数字。如果参数只写成宽 keyof T,返回值可能成为多个字段类型的联合。keyof 不是运行时枚举函数,也不保证对象恰好拥有这些自有可枚举键;索引签名、可选字段以及数字和符号键都需要结合实际契约理解。
- K 约束本次键选择,T 描述被读取的对象
- T[K] 把选中的键与对应字段值连接起来
- 类型中的键集合不等于 Object.keys 的实际结果
为什么还需要第二个类型参数
只让函数接收 object 和 string,编译器无法证明任意字符串都是这个对象的合法键。T 保留对象结构,K 再捕获本次传入的键,两个参数之间的约束让实现可以安全表达属性访问,并让返回值跟随具体选择。
示例从同一用户对象分别读取标识和名称,调用结果拥有各自的字段类型。拼错键名会被拒绝。若调用方本来持有两个键的联合,返回值也会对应这两个字段的联合,这是输入信息不确定性的正常反映,不能强行期待某一个固定字段类型。
function read<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = { id: 7, name: 'Lin' };
const id = read(user, 'id');
const name = read(user, 'name');
// @ts-expect-error nick 不在当前对象的键集合中
read(user, 'nick');
const optional: { label?: string } = {};
const label = read(optional, 'label');id 被推断为数字,name 为字符串,可选 label 的读取结果保留 undefined。预期错误确认不存在的键会被检查,函数没有声称能够自动验证任意外部对象的真实结构。
键并不总是字符串字面量
对象键在类型中可能包括字符串、数字或 unique symbol。带字符串索引签名的对象,其 keyof 可能包含 string 与 number,因为 JavaScript 的数字属性访问也会对应字符串形式的键。泛型工具若只支持字符串,应把这一限制写清楚。
对联合对象使用 keyof 时,能够直接操作的键也要考虑所有可能成员的共同契约,不能简单理解为收集任意分支全部键。需要读取分支专有属性时,应先收窄对象或设计相应类型变换,而不是把任意字符串断言成 keyof。
为什么 Object.keys 不能随意断言
运行时对象可能通过结构兼容携带类型声明之外的字段,Object.keys 又只枚举自有可枚举字符串键,不包含符号键。这与 keyof 所描述的静态使用能力并不是同一个集合。全局把所有 Object.keys 返回值断言成 keyof 数组,会隐藏这种差异。
如果工具控制了对象创建和字段集合,可以在小范围内提供经过论证的键辅助函数;开放输入则应检查键或只读取明确白名单。静态合法键也不代表字段值已经符合外部协议,缺字段、错误类型与动态修改仍属于运行时边界的责任。
容易答错的地方
- keyof 会在浏览器中返回所有对象键
- 它是类型运算,编译后没有枚举行为;实际枚举需要运行时 API,而且自有、可枚举与符号键规则可能与静态集合不同。
- 键是合法的就能保证读取结果有值
- 可选字段可能被省略,索引签名与相关编译选项也会影响未命中读取;应保留返回类型中的空值可能并按业务处理。
面试官还会怎么问?
为什么传入联合键后返回值变成联合?
调用者没有确定具体选中哪一个字段,函数只能保留所有可能返回值;若后续需要专有操作,应进一步缩小键或结果的范围。
泛型读取函数会校验服务端字段吗?
不会,输入声明必须由其他边界保证;读取函数只在已有类型契约内维持对象、键与值之间的关系,不负责解析未知响应。
只想接受字符串键应该怎么表达?
可把 K 限制到 keyof T 与 string 的交叉集合,明确拒绝数字或符号键;同时确认这种限制符合工具真实支持的运行时访问方式。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。