TypeScript 杂记十一 《Assert Array Index》
Posted 左手121
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TypeScript 杂记十一 《Assert Array Index》相关的知识,希望对你有一定的参考价值。
TypeScript 杂记十一 《Assert Array Index》
简介
- 在获取数组中某一项的值时候,如下:
const numbers = [5, 7];
console.log(numbers[1].toFixed());
- TS 不会以任何方式检查我们正在访问数组的实际索引处的元素,如下使用会报错
const numbers = [5, 7];
// 校验不报错,但是运行报错
console.log(numbers[100].toFixed());
- 在
TS4.1
开始新加了一个配置项noUncheckedIndexedAccess
,开启之后就会去推断对应数组实际索引的选项:
const numbers = [5, 7];
// 报错:对象可能为“未定义”。ts(2532)
console.log(numbers[1].toFixed());
// 正确
console.log(numbers[1]?.toFixed());
- 但是我们实际上在循环中是这样使用的,如下:我们可以很确定的知道他不会超出,也不会报错
const numbers = [5, 7];
for (let i = 0; i < numbers.length; i += 1)
// 报错:对象可能为“未定义”。ts(2532)
console.log(numbers[i].toFixed());
// 正确
console.log(numbers[i]?.toFixed());
- 因此我们需要定义一个
assertArrayIndex(array, key)
断言函数用来包装我们的数组,同时通过Index<typeof array>
来定义数组下标,使其可以使用。如下:(下一节我们来讲第二个参数的意义和作用)
const numbers = [5, 7];
assertArrayIndex(numbers, "numbers");
for (let i = 0 as Index<typeof numbers>; i < numbers.length; i += 1)
// 正确,允许使用 i
console.log(numbers[i].toFixed());
// 报错:对象可能为“未定义”。ts(2532)
console.log(numbers[0].toFixed());
// 正确
console.log(numbers[0]?.toFixed());
思路
- 我们先看一下下边的例子:
const numbers1 = [5, 7];
// 报错:对象可能为“未定义”。ts(2532)
numbers1[0].toFixed();
const numbers2 = [5, 7] as number[] & 0: number ;
// 正常
numbers2[0].toFixed();
const numbers3 = [5, 7] as number[] & aaaa: number ;
// 正常
numbers3.aaaa.toFixed();
- 通过上边的例子我们可以知道,我们给原本的数组添加一个
key: number
,这样我们就可以直接使用array[key]
去使用 - 因为数组的下标是一个数字,所以我们使用一个数字作为 key
- 最终效果如下:
assertArrayIndex(array, key)
生成100: number
Index<typeof array>
获取100
const numbers = [5, 7] as number[] & 100: number ;
for (let i = 0 as 100; i < numbers.length; i += 1)
console.log(numbers[i].toFixed());
- 为什么 assertArrayIndex 需要第二个参数
- 我们需要根据第二个参数生成这个数字,这个数字要保证唯一。为什么要保证唯一?
- 参考下例:
const matrix = [
[3, 4],
[5, 6],
[7, 8],
];
assertArrayIndex(matrix, "test");
let sum = 0;
for (let i = 0 as Index<typeof matrix>; i < matrix.length; i += 1)
const columns: number[] = matrix[i];
assertArrayIndex(columns, "test");
for (let j = 0 as Index<typeof columns>; j < columns.length; j += 1)
// 如果 i 和 j 重复,那么下边的使用不会报错
// 但是实际运行则会出现问题
const y: number = columns[i];
const u: number[] = matrix[j];
- 我们先去实现生成唯一值的函数
- 大致如下:不过有一个缺点,目前采用的加法,
aabb
和bbaa
结果一致。基于目前 TS 的机制,没有办法完全实现实现不同的字符串生成不同的 key。(至少我是没有想到解决的办法,无论加法、减法还是乘法都会出现) - 其实我们只要保证上述情况内唯一就行,所以即使重复也影响不大,只要我们保证在嵌套循环内使用不同的具有真实含义的单词就行
- 大致如下:不过有一个缺点,目前采用的加法,
type HashMapHelper<
T extends number,
R extends unknown[] = []
> = R["length"] extends T ? R : HashMapHelper<T, [...R, unknown]>;
type HashMap =
"0": HashMapHelper<0>;
"1": HashMapHelper<1>;
"2": HashMapHelper<2>;
"3": HashMapHelper<3>;
"4": HashMapHelper<4>;
"5": HashMapHelper<5>;
"6": HashMapHelper<6>;
"7": HashMapHelper<7>;
"8": HashMapHelper<8>;
"9": HashMapHelper<9>;
a: HashMapHelper<1>;
b: HashMapHelper<2>;
c: HashMapHelper<3>;
d: HashMapHelper<4>;
e: HashMapHelper<5>;
f: HashMapHelper<6>;
g: HashMapHelper<7>;
h: HashMapHelper<8>;
i: HashMapHelper<9>;
j: HashMapHelper<10>;
k: HashMapHelper<11>;
l: HashMapHelper<12>;
m: HashMapHelper<13>;
n: HashMapHelper<14>;
o: HashMapHelper<15>;
p: HashMapHelper<16>;
q: HashMapHelper<17>;
r: HashMapHelper<18>;
s: HashMapHelper<19>;
t: HashMapHelper<20>;
u: HashMapHelper<21>;
v: HashMapHelper<22>;
w: HashMapHelper<23>;
x: HashMapHelper<24>;
y: HashMapHelper<25>;
z: HashMapHelper<26>;
;
type Hash<
T extends string,
RR extends unknown[] = []
> = T extends `$infer L$infer R`
? Hash<R, [...RR, ...HashMap[keyof HashMap & L]]>
: RR["length"];
- 我们使用断言函数给原本的类型加上这个
key: number
function assertArrayIndex<A extends readonly unknown[], K extends string>(
array: A,
key: K
): asserts array is A & readonly [key in Hash<K>]: A[number]
- 不能在元组上调用该函数,我们先看一个示例
const A = [1, 2, 3];
type AA = typeof A; // number[]
type AAA = AA["length"]; // number
const B = [1, 2, 3] as const;
type BB = typeof B; // readonly [1, 2, 3]
type BBB = BB["length"]; // 3
- 根据上述的情况,我们来解决元组的问题
function assertArrayIndex<A extends readonly unknown[], K extends string>(
array: number extends A["length"] ? A : never,
key: K
): asserts array is number extends A["length"]
? A & readonly [key in Hash<K>]: A[number]
: never
- 之前我们生成的 key 要求是 0-9a-z 的字母组成的单词,且必填,我们来实现这个
type IsKeyHelper<K extends string> = K extends `$infer L$infer R`
? L extends keyof HashMap
? IsKeyHelper<R>
: false
: true;
type IsKey<K extends string> = K extends "" ? false : IsKeyHelper<K>;
function assertArrayIndex<A extends readonly unknown[], K extends string>(
array: number extends A["length"] ? A : never,
key: IsKey<K> extends true ? K : never
): asserts array is number extends A["length"]
? A & readonly [key in Hash<K>]: A[number]
: never
- 实现 Index,因为 Index 需要获取到对应的数字,因此我们需要通过一个约定的值去获取,如下:采用 symbol
declare const KEY: unique symbol;
function assertArrayIndex<A extends readonly unknown[], K extends string>(
array: number extends A["length"] ? A : never,
key: IsKey<K> extends true ? K : never
): asserts array is number extends A["length"]
? A & readonly [KEY]: Hash<K> &
readonly [key in Hash<K>]: A[number];
: never
type Index<Array extends readonly [KEY]: number > = Array[typeof KEY];
完整示例
type HashMapHelper<
T extends number,
R extends unknown[] = []
> = R["length"] extends T ? R : HashMapHelper<T, [...R, unknown]>;
type HashMap =
"0": HashMapHelper<0>;
"1": HashMapHelper<1>;
"2": HashMapHelper<2>;
"3": HashMapHelper<3>;
"4": HashMapHelper<4>;
"5": HashMapHelper<5>;
"6": HashMapHelper<6>;
"7": HashMapHelper<7>;
"8": HashMapHelper<8>;
"9": HashMapHelper<9>;
a: HashMapHelper<1>;
b: HashMapHelper<2>;
c: HashMapHelper<3>;
d: HashMapHelper<4>;
e: HashMapHelper<5>;
f: HashMapHelper<6>;
g: HashMapHelper<7>;
h: HashMapHelper<8>;
i: HashMapHelper<9>;
j: HashMapHelper<10>;
k: HashMapHelper<11>;
l: HashMapHelper<12>;
m: HashMapHelper<13>;
n: HashMapHelper<14>;
o: HashMapHelper<15>;
p: HashMapHelper<16>;
q: HashMapHelper<17>;
r: HashMapHelper<18>;
s: HashMapHelper<19>;
t: HashMapHelper<20>;
u: HashMapHelper<21>;
v: HashMapHelper<22>;
w: HashMapHelper<23>;
x: HashMapHelper<24>;
y: HashMapHelper<25>;
z: HashMapHelper<26>;
;
type Hash<
T extends string,
RR extends unknown[] = []
> = T extends `$infer L$infer R`
? Hash<R, [...RR, ...HashMap[keyof HashMap & L]]>
: RR["length"];
type IsKeyHelper<K extends string> = K extends `$infer L$infer R`
? L extends keyof HashMap
? IsKeyHelper<R>
: false
: true;
type IsKey<K extends string> = K extends "" ? false : IsKeyHelper<K>;
declare const KEY: unique symbol;
function assertArrayIndex<A extends readonly unknown[], K extends string>(
array: number extends A["length"] ? A : never,
key: IsKey<K> extends true ? K : never
): asserts array is number extends A["length"]
? A & readonly [KEY]: Hash<K> &
readonly TypeScript 杂记十一 《Assert Array Index》