Ben*_*ght 21 narrowing typescript
给定一个带有单个string[]
参数的函数myArray
。
如果我评估该.length
属性并且该属性大于 0,那么(假设我的变量没有any
伪装)它不可能是myArray[0]
未定义的。但是,如果我启用noUncheckedIndexedAccess
,它的类型将是string | undefined
const myFunc = (myArray: string[]) => {
if(myArray.length > 0) {
const foo = myArray[0] // now typed at 'string | undefined'
}
}
Run Code Online (Sandbox Code Playgroud)
现在我可以将 if 语句更改为评估myArray[0]
,并undefined
按照您的预期从类型中删除。但是,如果我现在想要检查数组的长度是否大于 6,该怎么办?我不想对索引 0-5 执行相同的操作来正确缩小类型范围。例如:
const myFunc = (myArray: string[]) => {
if(myArray[0] && myArray[1] && myArray[2] && myArray[3] && myArray[4] && myArray[5]) {
const foo = myArray[0] // now typed at 'string', but this is uggggly
}
}
Run Code Online (Sandbox Code Playgroud)
是否有一种更优雅的方法可以根据数组的长度缩小类型范围,或者我是否必须考虑为 TypeScript 代码库做出贡献?
jca*_*alz 26
正如您所怀疑的,TypeScript 不会根据数组属性的检查自动缩小数组的类型length
。这之前已在microsoft/TypeScript#38000中提出过建议,被标记为“太复杂”。看起来它之前已经在microsoft/TypeScript#28837上提出过建议,该建议仍然开放并标记为“等待更多反馈”。也许您可以转到该问题并留下反馈,说明为什么这样的事情对您有帮助以及为什么当前的解决方案不够充分,但我不知道它会有多大效果。不管怎样,我怀疑 TS 团队现在是否正在接受 Pull 请求来实现这样的功能。
在没有任何自动缩小的情况下,您可以编写一个具有您想要的效果的用户定义的类型保护函数。这是一种可能的实现:
type Indices<L extends number, T extends number[] = []> =
T['length'] extends L ? T[number] : Indices<L, [T['length'], ...T]>;
type LengthAtLeast<T extends readonly any[], L extends number> =
Pick<Required<T>, Indices<L>>
function hasLengthAtLeast<T extends readonly any[], L extends number>(
arr: T, len: L
): arr is T & LengthAtLeast<T, L> {
return arr.length >= len;
}
Run Code Online (Sandbox Code Playgroud)
该Indices<L>
类型旨在采用单个、相对较小、非负整型数字文字类型 L
,并返回具有 length 的数组的数字索引的并集L
。另一种说法是Indices<L>
应该是小于 的非负整数的并集L
。观察:
type ZeroToNine = Indices<10>
// type ZeroToNine = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Run Code Online (Sandbox Code Playgroud)
这种类型的工作原理是利用递归条件类型和可变元组类型0
从up走到L
。递归类型往往工作得很好,直到它们不能正常工作为止,这里的一个重要警告是,如果您传递L
太大的、或小数、或负数、或、number
或并集,事情会很奇怪,甚至抛出错误。对于这种方法来说,这是一个很大的警告。
接下来,LengthAtLeast<T, L>
接受一个数组类型T
和一个 length L
,并返回一个已知在至少 length 的数组的所有索引处都具有属性的对象L
。像这样:
type Test = LengthAtLeast<["a"?, "b"?, "c"?, "d"?, "e"?], 3>
/* type Test = {
0: "a";
1: "b";
2: "c";
} */
type Test2 = LengthAtLeast<string[], 2>
/* type Test2 = {
0: string;
1: string;
} */
Run Code Online (Sandbox Code Playgroud)
最后hasLengthAtLeast(arr, len)
是类型保护函数。如果返回true
,则arr
类型从 缩小T
为T & LengthAtLeast<T, L>
。让我们看看它的实际效果:
const myFunc = (myArray: string[]) => {
if (hasLengthAtLeast(myArray, 6)) {
myArray[0].toUpperCase(); // okay
myArray[5].toUpperCase(); // okay
myArray[6].toUpperCase(); // error, possibly undefined
}
}
Run Code Online (Sandbox Code Playgroud)
看起来不错。编译器很乐意允许您将myArray[0]
和视为myArray[5]
已定义的,但myArray[6]
仍然可能是未定义的。
无论如何,如果您确实决定使用类型保护,您可能需要平衡复杂性和需要使用它的程度。如果您只检查几个地方的长度,那么仅使用像这样的非null
断言运算符可能是值得的myArray[0]!.toUpperCase()
,而不必担心让编译器为您验证类型安全性。
或者,如果您无法控制 的值len
,那么您可能不想要一个脆弱的递归条件类型,而是构建一些更健壮但不太灵活的东西(比如可能只适用于特定len
值的重载类型保护,例如在评论中)微软/TypeScript#38000)。
这一切都取决于您的用例。