让我们在一些属性上构建一个示例记录。
type HumanProp =
| "weight"
| "height"
| "age"
type Human = Record<HumanProp, number>;
const alice: Human = {
age: 31,
height: 176,
weight: 47
};
对于每个属性,我还想添加一个人类可读的标签:
const humanPropLabels: Readonly<Record<HumanProp, string>> = {
weight: "Weight (kg)",
height: "Height (cm)",
age: "Age (full years)"
};
现在,使用这个Record类型和定义的标签,我想迭代具有相同键类型的两个记录。
function describe(human: Human): string {
let lines: string[] = [];
for (const key in human) {
lines.push(`${humanPropLabels[key]}: ${human[key]}`);
}
return lines.join("\n");
}
但是,我得到一个错误:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Readonly<Record<HumanProp, string>>'.
No index signature with a parameter of type 'string' was found on type 'Readonly<Record<HumanProp, string>>'.
如何在Typescript中正确实现此功能?
澄清一下,我正在寻找的解决方案,不管它是使用Record
类型、普通对象、类型、类、接口还是其他东西,都应该具有以下属性:
1.当我想定义一个新属性时,我只需要在一个地方做(就像上面的HumanProp),而不需要重复。
1.定义了一个新属性后,所有应该为该属性添加新值的地方,比如创建alice
或humanPropLabels
时,都会在编译时而不是运行时显示类型错误。
1.当我创建一个新属性时,迭代所有属性的代码,比如describe
函数,应该保持不变。
用Typescript的类型系统实现这样的东西有可能吗?
3条答案
按热度按时间6tdlim6h1#
我认为正确的方法是创建一个键名称的不可变数组,并给予它一个窄类型,这样编译器就可以识别它包含字符串类型,而不仅仅是
string
,使用const
Assert最容易做到这一点:然后你可以用它来定义
HumanProp
:代码的其余部分基本上可以按原样工作,只是在迭代键时,应该使用上面的不可变数组,而不是
Object.keys()
:不使用
Object.keys()
的原因是编译器无法验证Human
类型的对象是否 * 仅 * 具有Human
中声明的键。TypeScript中的对象类型是open/extendible,而不是closed/exact。这允许接口扩展和类继承工作:你不希望
describe()
因为你传入了一个SuperHero
而爆炸,SuperHero
是Human
的一种特殊类型,它有一个额外的powers
属性,所以你不应该使用Object.keys()
,它会正确地产生string[]
,你应该使用已知属性的硬编码列表,这样像describe()
这样的代码就会忽略任何额外的属性,如果它们存在的话。而且,如果向
humanProps
添加一个元素,您将在需要的位置看到错误,而describe()
将保持不变:好吧,希望能有所帮助;祝你好运!
Playground代码链接
a0zr77ik2#
有时候你不得不弯曲于使用Typescript......其他时候,让Typescript屈从于你可能会更好。我会选择一些比试图维护一些具体的字符串数组示例简单得多的东西(并保持它与接口同步)。我不会使用
HumanProps
。在我看来,这个简单的更改(添加as keyof Human
)是避免错误的最佳方法(而且IMO甚至使代码更具可读性):camsedfj3#
对于上面这个特定的问题,已经有了很好的答案,但是,在我的例子中,我的错误是一开始就试图使用记录类型。
退一步说,如果你试图将键Map到值,并以类型安全的方式迭代它们,你最好使用Map:
或者说,解决我的,简单得多的,问题的方法是:
不管怎么说,这个洞察力帮助了我。也许它也能帮助别人。