Description
noUncheckedIndexedAccess
其实没啥聊的, 这个配置属于 4.1 版本的一个新 feature, 该属性的作用在于使用索引访问某类型属性时, 该类型属性会被加上 undefined
类型:
Turning on
noUncheckedIndexedAccess
will addundefined
to any un-declared field in the type.
举个例子: 在没有该配置的情况下, 访问一个普通的 array 的类型如下:
const arr = [1, 2, 3] // `arr` type is `number[]`
const x = arr[0] // `x` type is `number`
const y = arr[1000] // `y` type is `number`
实际上这样的类型是不特别准确的, 由于 arr
这个数组长度没有限制, ts 没有足够的信息去判断其元素到底有多少个, 因此即使索引超过初始化定义的长度, ts 还是认为该元素"存在"
noUncheckedIndexedAccess
这一配置能够在使用索引访问时, 添加 undefined
类型:
const arr = [1, 2, 3] // `arr` type is `number[]`
const x = arr[0] // `x` type is `number | undefined`
const y = arr[1000] // `y` type is `number | undefined`
可以看到开启配置之后, 任何索引的访问都会带上 undefined
类型, 毕竟 ts 此时也不确定 arr 具体的形状是啥样了, 甚至是一个空 array 都是有可能的.
Tuple & const assertions
那如何定义固定长度的 array 类型呢? 一种方法是定义为 Tuple 类型:
const x: [number, number, number] = [1, 2, 3]
const x = arr[0] // `x` type is `number`
const y = arr[1000] // error: Tuple type '[number, number, number]' of length '3' has no element at index '9'.
当索引溢出时, 直接编译报错
第二种方法是使用 const assertions, 即断言成字面量类型:
const arr = [1, 2, 3] as const // `arr` type is `readonly [1, 2, 3]`
const x = arr[0] // `x` type is `number`
const y = arr[1000] // error: Tuple type '[number, number, number]' of length '3' has no element at index '9'.
使用这种类型断言之后, 类型会自动加上 readonly
关键字, ts 编译器就知道这种数据类型是 immutable
的, 自然长度也不会变了
以上两种方法不管 noUncheckedIndexedAccess
有没有配置, 都能正确识别索引溢出的错误
自定义类型
然而有一种情况下识别出的元素类型还是存在 undefined
类型的.
这里保持 noUncheckedIndexedAccess
配置开启, 有以下代码:
type A = [1, 2, 3]
const a: A = [1, 2, 3]
declare const i: number
const b = a[i] // `b` type is `1 | 2 | 3 | undefined`
我们仍旧使用 Tuple
类型声明数组, 并且严格规定其元素. 但访问的索引给定一个比较宽泛的类型 number
, 这时候去拿数组里的元素时发现他的类型又给出了 undefined
类型
真实的场景会有, 当使用 map()
, forEach()
这些方法时, 可能会有(其实并没有)如下代码:
a.forEach((v, i) => {
const b = a[i] // `b` type is `1 | 2 | 3 | undefined`
})
这时候 v
类型是 1 | 2 | 3
, 但是我自己用索引拿到的元素 b
类型却是 1 | 2 | 3 | undefined
, 有点困惑...
我想 ts 编译器应该也困惑吧, 毕竟索引类型是 number
了, 它也不知道具体是哪个索引, 所以我只能又给一个 undefined
, 虽然我数组的类型是严格的 Tuple
所以这里的索引 i
需要更加准确, 如果手动给的话, 即为 0 | 1 | 2
. 但是该如何根据已知的数组 a
和它的类型 A
自动推断出对应的索引类型呢?
比较通用并且容易想到的一种做法是用 keyof
:
declare const i: Exclude<keyof A, keyof typeof Array.prototype> // "0" | "1" | "2"
虽然最后的结果类型是 string
, 但是对于索引已经够用了
IndexOf
另一种做法就是自定义一个类型, 这里叫 IndexOf
, 该类型接受一个类型参数, 即数组的类型, 返回值是该数组的索引类型, 这里先看实现:
type IndexOf<
T extends any[],
S extends number[] = []
> = T["length"] extends S["length"]
? S[number]
: IndexOf<T, [...S, S["length"]]>;
使用就非常简单了
type A = [1, 2, 3]
const a: A = [1, 2, 3]
declare const i: IndexOf<A> // 0 | 1 | 2
const b = a[i] // `b` type is `3 | 1 | 2`
最后得到的类型顺序不重要(我觉得...)
简单讲一下过程:
T
和S
是两个泛型参数,T
的形状是一个 array,S
类型为数字数组, 默认为一个空数组类型. 这个S
存放了每次递归的状态, 这里后面会讲- 第一个条件, 也就是
?
前的extends
语句, 这个语句是递归的终止条件, 满足条件为当T
的长度和S
的长度一样 - 然后是
?
后的第一条语句, 这条语句为返回结果, 返回的是S[number]
, 也就是对S
取里面所有元素的 union 的值 - 最后是递归触发, 每次递归发生时让
S
自增, 从一开始的[]
, 一直到每次递增添加一个数字, 所以最后的S
值为[0, 1, 2]
最后发现了一个可以做类型体操的地方: type-challenges