TypeScript模板字面量作为对象键的类型推断异常问题及解法
问题描述
第一个示例中,对象键的类型推断符合预期:
const a = 'a' const obj = { [a]: 1 } // 类型为 { a: 1 }
但第二个示例出现类型推断异常:
const b = 'b/a' as `b/${string}` const obj2 = { [b]: 1 } // 实际类型为 { [x: string]: 1 }, 期望类型为 { [x: `b/${string}`]: 1 } const c = <T extends Record<string, unknown>>(arg: T): (keyof T & string)[] => { return Object.keys(arg) } const d = c(obj2) // 实际类型为 string[], 期望类型为 (`b/${string}`)[]
原因分析
TypeScript对计算属性的类型推断有明确规则:
- 当计算属性的键是具体字符串字面量(比如第一个示例中的
'a'),TypeScript会保留该具体字符串作为对象的键类型; - 当计算属性的键是宽字面量类型(比如
b/${string}这种包含通配符的模板字面量类型),TypeScript会默认将其退化为string类型的索引签名——因为编译期无法确定键的具体值,只能推断出键属于string范畴。
另外,Object.keys()的返回类型被TypeScript硬编码为string[],这是因为JavaScript中对象的键在运行时总会被转换为字符串,TypeScript无法保证静态类型中的键类型与运行时完全一致,所以默认返回最安全的string[]。
解决方法
方法1:使用泛型辅助函数创建对象
通过泛型约束让TypeScript保留键的模板字面量类型:
function createTypedObj<K extends string>(key: K, value: number): { [P in K]: number } { return { [key]: value } as { [P in K]: number }; } const b = 'b/a' as `b/${string}`; const obj2 = createTypedObj(b, 1); // 类型为 { [x: `b/${string}`]: number } const c = <T extends Record<string, unknown>>(arg: T): (keyof T & string)[] => { return Object.keys(arg) as (keyof T & string)[]; // 手动断言类型 }; const d = c(obj2); // 类型为 (`b/${string}`)[]
方法2:直接断言对象类型
如果不想写辅助函数,可以直接对创建的对象进行类型断言:
const b = 'b/a' as `b/${string}`; const obj2 = { [b]: 1 } as { [K in typeof b]: number }; // 类型为 { [x: `b/${string}`]: number } const c = <T extends Record<string, unknown>>(arg: T): (keyof T & string)[] => { return Object.keys(arg) as (keyof T & string)[]; }; const d = c(obj2); // 类型为 (`b/${string}`)[]
需要注意:对Object.keys()的返回值进行类型断言是必要的,这是TypeScript语言设计上的保守策略——避免运行时出现类型不匹配的风险,所以无法自动将返回值收窄为keyof T类型。
内容的提问来源于stack exchange,提问作者Acid Coder
相关产品推荐
相关产品推荐

