TypeScript 杂记十一 《Assert Array Index》

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];
// 报错:对象可能为“未定义”。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];
  }
}
  • 我们先去实现生成唯一值的函数
    • 大致如下:不过有一个缺点,目前采用的加法,aabbbbaa 结果一致。基于目前 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 [key in Hash<K>]: A[number];
    }
  : never {}

type Index<Array extends { readonly [KEY]: number }> = Array[typeof KEY];
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值