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 ] ;