TypeScript 杂记十一 《Assert Array Index》
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];
console.log(numbers[1].toFixed());
console.log(numbers[1]?.toFixed());
- 但是我们实际上在循环中是这样使用的,如下:我们可以很确定的知道他不会超出,也不会报错
const numbers = [5, 7];
for (let i = 0; i < numbers.length; i += 1) {
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) {
console.log(numbers[i].toFixed());
console.log(numbers[0].toFixed());
console.log(numbers[0]?.toFixed());
}
思路
const numbers1 = [5, 7];
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) {
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;
type AAA = AA["length"];
const B = [1, 2, 3] as const;
type BB = typeof B;
type BBB = BB["length"];
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 [key in Hash<K>]: A[number];
}
: never {}
type Index<Array extends { readonly [KEY]: number }> = Array[typeof KEY];
|