TypeScript的预定义类型在lib.d.ts
,通常是非常好的类型,并给出了大量关于如何使用内置功能的信息,以及为你提供了额外的类型安全。直到他们不这样做。考虑下面的例子,对象类型是Person
。
type Person = {
name: string, age: number, id: number,
}
declare const me: Person;
Object.keys(me).forEach(key => {
// 💥 the next line throws red squigglies at us
console.log(me[key])
})
我们有一个类型为Person
的对象,用Object.keys
,我们想获得所有的键为字符串,然后用这个来访问map
或forEach
循环中的每个属性,在严格模式下做一些事情,我们得到红色的斜线。这就是错误信息。
元素隐含有一个'any'类型,因为'string'类型的表达式不能用于索引'Person'类型。在'Person'类型上没有找到带有'string'类型参数的索引签名。
那么发生了什么?Object.keys
的类型声明如下。
interface ObjectConstructor {
//...
keys(o: object): string[]
keys(o: {}): string[]
}
两个重载都接受任何对象作为输入,并返回一个字符串数组作为输出。这是正确的和预期的行为。它只是对我们已经知道的东西非常概括,而且TypeScript应该知道更多。
string是我们可以从Person
中访问的实际键的超集。具体的子集是name | age | id
。这也是TypeScript允许我们从Person
中索引的值集。对于其他的字符串,TypeScript说它可能是,但索引的值可能是任何东西。而在严格模式下,除非明确说明,否则任何都是不允许的。
重要提示:这很可能是有原因的。要么是更具体的类型会在成熟的库中引起问题。或者行为太复杂,无法用一个类型来概括。或者,只是有更重要的事情。这并不意味着更好的类型不会在某个时候出现。
但是,我们仍然可以做什么?
选择1。类型铸造#
最糟糕的解决方案是关闭noImplicitAny
。这对错误和错误类型来说是一扇敞开的大门。最明显的解决方案是类型转换。我们可以把对象投给任何对象,以允许......一切发生。
Object.keys(me).forEach((key) => {
console.log((me as any)[key])
})
这并不酷。keyof Person
或者我们可以将key
,以确保TypeScript理解我们的目标。
Object.keys(me).forEach((key) => {
console.log(me[key as keyof Person])
})
更好。但还是不酷。这是TypeScript应该自己做的事情!所以如果TypeScript还不知道,我们可以开始教TypeScript如何做。
选项2.扩展对象构造器#
感谢接口的声明合并功能,我们可以用我们自己的类型定义扩展ObjectConstructor
接口。我们可以在我们需要的地方直接这样做,或者创建我们自己的环境声明文件。
我们打开接口,并为keys
写下另一个重载。这一次,我们想非常具体地了解我们进入的对象的值,并根据它的形状决定返回什么。
这就是我们的行为。
- 如果我们传递一个数字,我们得到一个空数组。
- 如果我们传入一个字符串或一个数组,我们会得到一个字符串数组的返回。这个字符串数组包含数字索引的字符串表示,用于索引数组或字符串的位置。意思是说,这个字符串数组的长度与它的输入相同。
- 对于任何真实的对象,我们返回它的键。
我们为此构造一个辅助类型。这个是一个条件类型,描述了上面的行为。
type ObjectKeys<T> =
T extends object ? (keyof T)[] :
T extends number ? [] :
T extends Array<any> | string ? string[] :
never;
在我的条件类型中,我通常在never上结束。这给了我第一个信号:我要么在声明中忘记了什么,要么在代码中做了完全错误的事情。在任何情况下,这是一个很好的指针,可以看出有什么东西在发臭。
现在,我们打开ObjectConstructor
接口,为键添加另一个重载。我们定义了一个通用类型的变量,返回值是基于条件类型ObjectKeys
。
interface ObjectConstructor {
keys<T>(o: T): ObjectKeys<T>
}
同样,由于这是一个接口,我们可以在我们需要的地方对我们的定义进行猴子式的修补。当我们把一个具体的对象传递给Object.keys
,我们就把通用类型变量T
绑定到这个对象。这意味着我们的条件可以给出关于返回值的确切信息。而且由于我们的定义是所有三个键声明中最具体的,TypeScript默认使用这个。
我们的小例子不再向我们扔方块字了。
Object.keys(me).forEach((key) => {
// typeof key = 'id' | 'name' | 'age'
console.log(me[key])
})
key
的类型现在是'id' | 'name' | 'age'
,就像我们希望的那样。另外,对于所有其他情况,我们得到了正确的返回值。
注意:传递一个数组或字符串的行为并没有明显的变化。但这是一个很好的指标,说明你的代码可能有问题。空数组也一样。尽管如此,我们还是保留了内置功能的行为。
扩展现有的接口是一种选择进入类型的好方法,因为某些原因,我们不能得到我们需要的信息。