Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单聊一聊 typescript 中的 noUncheckedIndexedAccess #21

Open
hacker0limbo opened this issue Feb 19, 2021 · 0 comments
Open

简单聊一聊 typescript 中的 noUncheckedIndexedAccess #21

hacker0limbo opened this issue Feb 19, 2021 · 0 comments
Labels
typescript typescript 笔记整理

Comments

@hacker0limbo
Copy link
Owner

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`

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

简单讲一下过程:

  • TS 是两个泛型参数, T 的形状是一个 array, S 类型为数字数组, 默认为一个空数组类型. 这个 S 存放了每次递归的状态, 这里后面会讲
  • 第一个条件, 也就是 ? 前的 extends 语句, 这个语句是递归的终止条件, 满足条件为当 T 的长度和 S 的长度一样
  • 然后是 ? 后的第一条语句, 这条语句为返回结果, 返回的是 S[number], 也就是对 S 取里面所有元素的 union 的值
  • 最后是递归触发, 每次递归发生时让 S 自增, 从一开始的 [], 一直到每次递增添加一个数字, 所以最后的 S 值为 [0, 1, 2]

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

参考

@hacker0limbo hacker0limbo added the typescript typescript 笔记整理 label Feb 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript typescript 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant