177. 断言数组索引合法性
极难
有时候我们希望使用传统的 for
循环和索引来遍历数组,但在这种情况下,TypeScript 并不会检查我们是否在真实的索引上访问数组元素(即没有超过数组的长度),也不会检查我们是否使用了一个任意的数字作为索引,或者来自其他数组的索引(比如在嵌套循环、遍历矩阵或图时):
const matrix = [
[3, 4],
[5, 6],
[7, 8],
]
// 在没有启用 noUncheckedIndexedAccess 选项时,这个例子不会产生类型错误。
for (let i = 0; i < matrix.length; i += 1) {
const columns: number[] = matrix[i]
for (let j = 0; j < columns.length; j += 1) {
const current: number = columns[i] // 错误!应该是 j 不是 i
console.log(current.toFixed()) // TypeError: Cannot read property 'toFixed' of undefined
}
}
你可以启用 noUncheckedIndexedAccess 选项(在 tsconfig.json
中),但每次访问数组元素时,你都需要检查该元素是否存在(即编写 if
语句),这会显得有些冗长且不方便,特别是对于这种 for
循环遍历的情况,我们可以确信索引不会超过数组的长度:
const numbers = [5, 7]
for (let i = 0; i < numbers.length; i += 1) {
const current = numbers[i]
if (current !== undefined) {
console.log(current.toFixed())
}
}
编写一个 assert
函数 assertArrayIndex(array, key)
,它可以应用于任何 array
(并且需要一个唯一的字符串 key
,用于在类型层面区分不同的数组),这样就可以通过特殊的泛型类型 Index<typeof array>
来访问数组元素,确保数组元素是存在的(这个功能要求启用 noUncheckedIndexedAccess 选项):
const numbers = [5, 7]
assertArrayIndex(numbers, 'numbers')
for (let i = 0 as Index<typeof numbers>; i < numbers.length; i += 1) {
console.log(numbers[i].toFixed())
}
通过这样的索引访问时,必须保证数组中有该元素,而通过其他任何索引进行访问时,则没有这种保证(元素可能不存在):
const matrix = [
[3, 4],
[5, 6],
[7, 8],
]
assertArrayIndex(matrix, 'rows')
let sum = 0
for (let i = 0 as Index<typeof matrix>; i < matrix.length; i += 1) {
const columns: number[] = matrix[i]
// @ts-expect-error: number | undefined 不能赋值给 number
const x: number[] = matrix[0]
assertArrayIndex(columns, 'columns')
for (let j = 0 as Index<typeof columns>; j < columns.length; j += 1) {
sum += columns[j]
// @ts-expect-error: number | undefined 不能赋值给 number
const y: number = columns[i]
// @ts-expect-error: number | undefined 不能赋值给 number
const z: number = columns[0]
// @ts-expect-error: number[] | undefined 不能赋值给 number[]
const u: number[] = matrix[j]
}
}
assertArrayIndex
函数不能用于元组(因为元组的元素访问已经在类型上有良好的检查):
const tuple = [5, 7] as const
// @ts-expect-error
assertArrayIndex(tuple, 'tuple')