TypeScript类型实现数组flat

2,146 阅读2分钟

前段时间在知乎看到大佬用typescript类型运算实现一个中国象棋程序, 才知道typescript可以这样玩。

image.png

看了下大佬的实现,发现了有个一直没用过的infer操作符。查了下infer 表示在 extends 条件语句中待推断的类型变量。

image.png

上面是typescript系统内置的ReturnType,可以推断出数据的类型,其中infer R可以理解为一个占位,也是返回的类型。

image.png

infer

知道infer的作用后我们来练习下用法

  • 取数组中的第一个元素
  • hello world中的hello

取数组中的第一个元素

type 数组1 = [686, 999];
type 获取数组的第一个元素<任意数组 extends any[]> = 任意数组 extends [
  第一个元素: infer 数组的第一个元素,
  ...剩余的元素: infer 剩余的数组
]
  ? 数组的第一个元素
  : any;

type 第一个元素是 = 获取数组的第一个元素<数组1>;

测试下, 拿到了686。

image.png

hello world中的hello

type 你好世界 = 'hello world';
type 获取世界 <字符串 extends string> = 字符串 extends `${infer R} world` ? R : any;
type 世界 = 获取世界<你好世界>

image.png

上面两个例子,说明了infer 表示在 extends 条件语句中待推断的类型变量

flat拍平数组

flat指拉平任意层数的数组,默认拉平一层

[1, [2, [3]]].flat() // [1, 2, [3]]

先学习下flat的实现, 我写了以下两种。

// 遍历加递归
function flat(arr, depth = 1) {
  if (depth === 0) return arr;
  return arr.reduce((r, c) => {
    if (Array.isArray(c)) {
      return [...r, ...flat(c, depth - 1)];
    }
    return [...r, c];
  }, []);
}

// 递归+解构
function flat(arr, depth = 1) {
    if (depth === 0 || arr.length === 0) return arr;
    const [t, ...rest] = arr;
    if (Array.isArray(t)) {
        return [
            ...flat(t, depth - 1),
            ...flat(rest, depth)
        ];
    }
    return [t, ...flat(rest, depth)];
}

typescript没有可以直接像reduce遍历的类型,因此只能用第二种方式来搞。

问题拆解

  • 用数组['length']表示depth
  • 类型生成depth长度的数组
  • 类型减一表示depth-1

用数组['length']表示depth

上面代码中会有depth - 1的操作,但是在typescript中类型是不能直接减一的,这里可以使用一个技巧,通过数组的length表示长度。

type 长度是3的数组 = [any, any, any]
type 获取数组长度<数组 extends any[]> = 数组['length'];
type 三 = 获取数组长度<长度是3的数组>

image.png

类型生成depth长度的数组

那么我们只要有一个类型生成任意长度数组,就可以表示一个数字了。这里只需要递归遍历下即可

type 生成对应长度的空数组<长度, 结果数组 extends any[] = []> = 结果数组["length"] extends 长度
    ? 结果数组
    : 生成对应长度的空数组<长度, [any, ...结果数组]>;

type 长度为3的数组 = 生成对应长度的空数组<3>;

image.png

类型减一表示depth-1

要实现depth - 1的操作,不就是depth长度的数组减去一个元素,变成depth - 1长度的数组。这里就用到了infer

// 使用解构把数组的第一个元素去掉
type 数组长度减一<数组 extends any[] = []> = 数组 extends [
    第一个元素: infer 数组的第一个元素,
    ...剩余的元素: infer 剩余的数组
] ? 剩余的数组 : 数组;

type 减一<N extends number> = 获取数组长度<数组长度减一<生成对应长度的空数组<N>>>;
type 九 = 减一<10>

image.png

这样就实现了depth - 1

实现flat拉平任意层级数组

有了上面的类型,就可以根据flat函数的实现原理来完成typescript版本的flat了,主要对着函数逻辑来extends实现if else语句

image.png

type 拉平任意层级数组<
  多维数组 extends any[],
  层数 extends number = 1
> = 层数 extends 0
  ? 获取数组长度<多维数组> extends 0
    ? 多维数组
    : 多维数组
  : 多维数组 extends [
      第一个元素: infer 数组的第一个元素,
      ...剩余的元素: infer 剩余的数组
    ]
  ? 数组的第一个元素 extends any[]
    ? [
        ...拉平任意层级数组<数组的第一个元素, 减一<层数>>,
        ...拉平任意层级数组<剩余的数组, 层数>
      ]
    : [数组的第一个元素, ...拉平任意层级数组<剩余的数组, 层数>]
  : 多维数组;

来个复杂的数组拉平两层测试下, OK

image.png

完整代码

type 生成对应长度的空数组<
  长度,
  结果数组 extends any[] = []
> = 结果数组["length"] extends 长度
  ? 结果数组
  : 生成对应长度的空数组<长度, [any, ...结果数组]>;

type 数组长度减一<数组 extends any[] = []> = 数组 extends [
  第一个元素: infer 数组的第一个元素,
  ...剩余的元素: infer 剩余的数组
]
  ? 剩余的数组
  : 数组;

type 减一<N extends number> = 获取数组长度<
  数组长度减一<生成对应长度的空数组<N>>
>;

type 拉平任意层级数组<
  多维数组 extends any[],
  层数 extends number = 1
> = 层数 extends 0
  ? 获取数组长度<多维数组> extends 0
    ? 多维数组
    : 多维数组
  : 多维数组 extends [
      第一个元素: infer 数组的第一个元素,
      ...剩余的元素: infer 剩余的数组
    ]
  ? 数组的第一个元素 extends any[]
    ? [
        ...拉平任意层级数组<数组的第一个元素, 减一<层数>>,
        ...拉平任意层级数组<剩余的数组, 层数>
      ]
    : [数组的第一个元素, ...拉平任意层级数组<剩余的数组, 层数>]
  : 多维数组;


type 数组 = 拉平任意层级数组<[[0], [1, [2, [[3, [4]]], [5, [6], 7]]], [8, [9]], 10], 2>;

优化

上面的减一实现最多只能100,然后递归就爆了, 换了一种方式实现类型生成对应长度的数组, 最大支持到生成9999长度的数组

type 生成对应长度的空数组<
    长度 extends string,
    结果数组 extends any[] = []
    > = `${结果数组["length"]}` extends 长度
    ? 结果数组
    : 生成对应长度的空数组<长度, [any, ...结果数组]>;

// 打表
type 字符串数字 = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';


// 按照乘法*10来叠加数组长度
// 11 = 1 * 10 + 1
// 111 = 10 * 10 + 1 * 10 + 1
type 创建对应长度的数组<长度 extends string, 结果数组 extends any[] = []> =
    长度 extends `${infer 第一个数字}${infer 剩余数字}`
    ? 第一个数字 extends 字符串数字
    ? 创建对应长度的数组<剩余数字, [...结果数组, ...结果数组, ...结果数组, ...结果数组, 
    ...结果数组, ...结果数组, ...结果数组, ...结果数组, ...结果数组, ...结果数组, 
    ...生成对应长度的空数组<第一个数字>]>
    : never
    : 结果数组
type 数组长度减一<数组 extends any[] = []> = 数组 extends [
    第一个元素: infer 数组的第一个元素,
    ...剩余的元素: infer 剩余的数组
]
    ? 剩余的数组
    : 数组;

type 减一<长度 extends number> = 获取数组长度<
    数组长度减一<创建对应长度的数组<`${长度}`>>
>;


type 拉平任意层级数组<
    多维数组 extends any[],
    层数 extends number = 1
    > = 层数 extends 0
    ? 获取数组长度<多维数组> extends 0
    ? 多维数组
    : 多维数组
    : 多维数组 extends [
        第一个元素: infer 数组的第一个元素,
        ...剩余的元素: infer 剩余的数组
    ]
    ? 数组的第一个元素 extends any[]
    ? [
        ...拉平任意层级数组<数组的第一个元素, 减一<层数>>,
        ...拉平任意层级数组<剩余的数组, 层数>
    ]
    : [数组的第一个元素, ...拉平任意层级数组<剩余的数组, 层数>]
    : 多维数组;


type 数组 = 拉平任意层级数组<[[0], [1, [2, [[3, [4]]], [5, [6], 7]]], [8, [9]], 10], 2>;

last-modify: 2022-01-10 更新

最后

上面的实现最关键是数字怎么增加或者减少和合理的函数逻辑。如果不熟悉可以先从简单点的入手,比如直接拉平为一维数组。拉平为一维数组的函数实现, 感兴趣可以使用infer实现下。

const flatern = (arr) => {
    if (arr.length === 0) return arr;
    const [top, ...rest] = arr;
    if (Array.isArray(top)) {
        return flatern([...top, ...rest]);
    }
    return [top, ...flatern(rest)];
};