hacker0limbo / my-blog

个人博客
5 stars 1 forks source link

简单聊一聊 typescript 中的 noUncheckedIndexedAccess #21

Open hacker0limbo opened 3 years ago

hacker0limbo commented 3 years ago

noUncheckedIndexedAccess

其实没啥聊的, 这个配置属于 4.1 版本的一个新 feature, 该属性的作用在于使用索引访问某类型属性时, 该类型属性会被加上 undefined 类型:

Turning on noUncheckedIndexedAccess will add undefined 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`

最后得到的类型顺序不重要(我觉得...)

简单讲一下过程:

最后发现了一个可以做类型体操的地方: type-challenges

参考