给一个前置不定量参数函数补充类型定义

有一个比较复杂的函数,它的前面的参数是不定量的,但是类型相同都是字符串,最后一个参数是一个回调函数,而回调函数的参数的数量和前面字符串的属性相同,且类型为该字符串描述的类型。

addImpl("string", "boolean", "number", (a, b, c) => {
}); //a: string, b: boolean, c: number

那我该怎么使用 TS 给这个函数补充类型定义呢?如果没什么思路的话,我们可以一点一点来想。 第一步肯定是用 TS 来声明这个函数,返回值很简单就是 void

declare function addImpl(): void

那么参数该怎么填呢?因为参数是不定量的,我们很容易想到使用 ... 来表示不定量参数:

declare function addImpl(...args: string[], callback: Function): void
//                       -> A rest parameter must be last in a parameter list.

但是这样写是有语法错误的,因为 ... 必须放在最后,所以想来想去也没有什么好的办法,只能选择把所有的参数都合并到一起,形成一个参数列表,前面是字符串,后面是回调函数。

declare function addImpl(...args: [...string[], Function]): void

这样就没有语法错误了,也能够实现一定程度的参数约束,比如函数前面的参数必须都是字符串,最后一个参数必须是一个函数。但是仍然没有做到最严格的类型约束,所以我们需要进一步优化。比如现在前面的字符串现在可以写任意的字符串,所以我们第一步先对前面的字符串进行约束,所以我们需要先定义一个类型,里面包含所有 JS 内置的类型。

type JSBuiltInType = "string" | "boolean" | "number" | "bigint" | "symbol" | "undefined" | "object" | "function"

然后把前面的字符串约束为这个类型:

declare function addImpl(...args: [...JSBuiltInType[], Function]): void

这样至少我们在写前面的字符串的时候,就不能随便写了。 接下来比较麻烦的问题就是标注后面的回调函数了,它的类型肯定不能是 Function。我们观察到回调函数的参数列表和前面的字符串是有关系的,它的个数和前面字符串的个数相同且类型对应,所以意味着这个参数也是不定量的,所以它的类型也应该是一个数组展开:

declare function addImpl(...args: [
  ...JSBuiltInType[],
  (...args: any[]) => void
]): void

而这个数据是和前面 JSBuiltInType[] 有关系的,所以相当与我们已知前面的类型,然后基于前面的类型进行运算,算出最终的类型。当两个地方的类型需要某种关联时,我们自然而然就会想到使用泛型。

declare function addImpl<T extends JSBuiltInType[]>(...args: [
    ...T,
    (...args: Transfer<T>) => void
]): void

那么最终我们的任务就变成了该怎么实现 Transfer<T> 这个类型。而 Transfer<T> 要做的事情也很简单,就是把字符串字面量描述的类型转换为实际的类型。

// from ["string", "boolean", "number"] to [string, boolean, number]
type Transfer<T extends JSBuiltInType[]>;

所以我们需要先创建一个 JS 类型的映射表,然后基于这个映射表进行类型转换。

type JSBuiltInTypeMap = {
  string: string;
  boolean: boolean;
  number: number;
  bigint: bigint;
  symbol: symbol;
  undefined: undefined;
  object: object;
  function: Function;
};

JSBuiltInType 的定义就可以简化了:

type JSBuiltInType = keyof JSBuiltInTypeMap;

然后我们就可以继续实现 Transfer<T> 了。

type Transfer<T extends JSBuiltInType[]> = {
  [K in T]: JSBuiltInTypeMap[T[K]];
};

最终我们实现了对 addImpl 的类型定义。完整代码如下:

type JSBuiltInTypeMap = {
  string: string;
  boolean: boolean;
  number: number;
  bigint: bigint;
  symbol: symbol;
  undefined: undefined;
  object: object;
  function: Function;
};
type JSBuiltInType = keyof JSBuiltInTypeMap;

type Transfer<T extends JSBuiltInType[]> = {
  [K in keyof T]: JSBuiltInTypeMap[T[K]];
};

declare function addImpl<T extends JSBuiltInType[]>(...args: [
    ...T,
    (...args: Transfer<T>) => void
]): void

addImpl("string", "boolean", "number", (a, b, c) => {
}); //a: string, b: boolean, c: number