TypeScript中如何约束对象属性值为该对象自身的键名
问题描述
假设我定义了如下对象:
const obj: Obj = { a: ['b'], b: ['c'] }
需要实现以下类型校验规则:
a: ['b']写法符合校验规则,因为b是当前对象的自有键b: ['c']写法需要被判定为类型错误,因为c不属于当前对象的自有键
目前尝试编写的类型定义如下:
type Obj = { [key: string]: (keyof Obj)[] }
使用该类型定义后,两处属性赋值都会抛出如下类型错误:
Type 'string' is not assignable to type 'never'
解决方案
出现never报错的核心原因是你写的Obj类型存在递归自引用,且没有固定的已知键,TypeScript推导时会认为Obj不存在任何确定属性,因此keyof Obj会被推导为never,自然无法赋值任何字符串值。
要实现「数组元素必须为当前对象自身存在的键」的自引用约束,不能直接靠固定的索引签名实现,需要借助泛型辅助函数做动态类型推导:
const createObj = <T extends Record<keyof T, (keyof T)[]>>(obj: T): T => obj; // 校验通过,符合预期 const obj = createObj({ a: ['b'] }) // 抛出类型错误:Type '"c"' is not assignable to type '"a" | "b"',符合预期 const badObj = createObj({ a: ['b'], b: ['c'] })
实现原理
- 泛型参数
T会在你传入对象字面量时,自动推导出对象的实际结构,此时keyof T可以准确拿到当前对象所有真实存在的键,不会出现递归索引签名推导为never的问题 - 泛型约束
T extends Record<keyof T, (keyof T)[]>会强制要求对象的每个属性值,都必须是由对象自身键组成的数组,自然可以拦截到不存在的键c - 辅助函数返回值的类型和直接书写对象字面量的类型完全一致,不会丢失任何属性类型信息
如果需要单独抽离Obj类型,可以配合泛型改写,不影响校验逻辑:
type Obj<T> = { [K in keyof T]: (keyof T)[] } const createObj = <T extends Obj<T>>(obj: T): T => obj;
内容的提问来源于stack exchange,提问作者Andy Jessop
相关产品推荐
相关产品推荐

