为何模板字面量类型无法用于对象属性名?如何为函数添加正确类型?
问题描述
我有如下简单JavaScript函数:
function retObj(suff, n) { const r = {[`fixed${suff}`]: n * 2}; return r; } retObj('A', 3) // { fixedA: 6 }
尝试为其添加TypeScript类型定义时,编写了如下代码:
type FixedWithSuffix<T extends string> = `fixed${T}`; type ObjWithFixedField<T extends string> = {[k in FixedWithSuffix<T>]: number}; function retObj<T extends string>(suff: T, n: number): ObjWithFixedField<T> { return {[`fixed${suff}`]: n * 2}; }
开启严格模式后,编译器报错:
Type '{ [x: string]: number; }' is not assignable to type '{ [k in `fixed${T}`]: number; }'.(2322)
但返回字符串的类似场景却可以正常工作:
type FixedWithSuffix<T extends string> = `fixed${T}`; function retStr<T extends string>(suff: T): `fixed${T}` { return `fixed${suff}`; }
报错原因
TypeScript对模板字面量的类型推断逻辑在字符串和对象场景下存在差异:
- 返回字符串时,
fixed${suff}会被精准推断为fixed${T}模板字面量类型,因为TS能直接关联模板表达式的泛型类型; - 返回对象时,使用计算属性创建的对象,其键的类型会被默认拓宽为
string——TS无法确保运行时该键不会被修改,所以采用更宽泛的类型兜底,导致返回值类型{ [x: string]: number }和预期的ObjWithFixedField<T>不匹配,触发报错。
正确的类型定义方案
以下是几种可行的解决方式:
方案1:直接使用类型断言
明确告知编译器返回对象的类型与目标类型匹配:
type FixedWithSuffix<T extends string> = `fixed${T}`; type ObjWithFixedField<T extends string> = {[k in FixedWithSuffix<T>]: number}; function retObj<T extends string>(suff: T, n: number): ObjWithFixedField<T> { return {[`fixed${suff}`]: n * 2} as ObjWithFixedField<T>; }
方案2:借助Object.assign创建对象
Object.assign的类型推断能更好地保留计算属性的字面量类型:
type FixedWithSuffix<T extends string> = `fixed${T}`; type ObjWithFixedField<T extends string> = {[k in FixedWithSuffix<T>]: number}; function retObj<T extends string>(suff: T, n: number): ObjWithFixedField<T> { return Object.assign({}, {[`fixed${suff}`]: n * 2}) as ObjWithFixedField<T>; }
方案3:先约束键的类型再创建对象
先把模板字符串的键约束为目标字面量类型,再生成对象:
type FixedWithSuffix<T extends string> = `fixed${T}`; type ObjWithFixedField<T extends string> = {[k in FixedWithSuffix<T>]: number}; function retObj<T extends string>(suff: T, n: number): ObjWithFixedField<T> { const key = `fixed${suff}` as FixedWithSuffix<T>; const obj = { [key]: n * 2 } as ObjWithFixedField<T>; return obj; }
方案4:利用TS 4.4+的as const约束
在TS 4.4及以上版本,可通过as const直接限制计算属性的键类型:
type FixedWithSuffix<T extends string> = `fixed${T}`; type ObjWithFixedField<T extends string> = {[k in FixedWithSuffix<T>]: number}; function retObj<T extends string>(suff: T, n: number): ObjWithFixedField<T> { return {[`fixed${suff}` as const]: n * 2} as ObjWithFixedField<T>; }
内容的提问来源于stack exchange,提问作者Marko
相关产品推荐
相关产品推荐

