What? Can function type overloading be dynamically generated?

Overloading means that a function can have different parameters and return values, that is, have different function signatures.

ts supports function overloading, you can define multiple different types for the same function:

There are three ways to write overloading (most people probably only have one):

declare function func(name: string): string;
declare function func(name: number): number;
复制代码

This is more commonly used by everyone. Declare two functions with the same name to achieve the purpose of overloading:

Functions can be declared as interfaces, and similarly, function overloads can be declared as interfaces:

The function type can take the cross type, that is, multiple types can be used, in fact, it also means function overloading:

Although overloading is a very useful feature, sometimes it is quite troublesome to write if there are too many overloads.

For example, there is such a type definition in lib.dom.ts provided by ts:

Because each parameter corresponds to a different return value, it is overloaded so much.

It's too troublesome to write like this, can you use type programming to dynamically generate it?

Consider the three ways to write overloading, declare and interface are not good, but & is OK, can I pass in a union type, and then it returns the intersection type?

For example:

It has been suggested that it is definitely possible, let's take a look at the implementation:

Union interchange

Function parameters have the property of contravariance, that is, the type is reduced. For example, if the parameters can be passed A, B, and C at the same time, how to define the parameter type?

It must be the intersection of A, B, and C, that is, the intersection type of your A & B & C, so that you can receive the parameters of A, B, and C.

This property can be exploited to achieve joint trans-intersection.

type UnionToIntersection<U> = 
    (U extends U ? (x: U) => unknown : never) extends (x: infer R) => unknown
        ? R
        : never
复制代码

have a test:

The type parameter U here is the incoming union type, and adding a U extends U is to trigger the characteristics of the distributed conditional type.

What is a distributed condition type?

When the type parameter is a union type and the type parameter is directly referenced on the left side of the conditional type, TypeScript will pass in each element separately for type operation, and finally merge it into a union type. This syntax is called distributed conditional type.

For example, a union type like this:

type Union = 'a' | 'b' | 'c';
复制代码

If we want to capitalize the a, we can write:

type UppercaseA<Item extends string> = 
    Item extends 'a' ?  Uppercase<Item> : Item;
复制代码

Back to this high-level type of union junction:

Adding a U extends U or U extends any can trigger the characteristics of the distributed conditional type, so that the union type is divided into each type and passed in separately for calculation, and finally the result is merged into a union type.

然后再把它放到函数参数的位置,构造一个函数类型,通过模式匹配的方式提取参数的类型到 infer 声明的局部变量 R 里返回。

这样的结果就是交叉类型。

原因上面说过了,函数参数有逆变的性质,传入联合类型会返回交叉类型。

实现了联合转交叉之后,函数重载也就可以写出来了:

比如三个重载的返回值分别是 Aaa、Bbb、Ccc:

我们想基于这个生成重载的类型定义,传入联合类型返回重载的函数:

就可以这样写:

type UnionToOverloadFunction<T extends keyof ReturnValueMap> = 
    UnionToIntersection<
        T extends any ? (type: T) => ReturnValueMap[T] : never
    >;
复制代码

类型参数 T 是 ReturnValueMap 里的 key,约束为 keyof ReturnValueMap。

通过 T extends any 触发联合类型在分布式条件类型中的分发特性,让 'aaa' 'bbb' 'ccc' 分别传入做计算,返回构造出的函数类型的联合。

我们先单独测试下这部分:

可以看到返回的是构造出的函数类型的联合类型。

然后就用上面的 UnionToIntersection 转交叉就可以了:

这样就实现了重载函数的动态生成:

对比下最开始那种写法:

是不是清爽多了!而且还可以写一些动态逻辑。

总结

ts 函数重载一共有三种写法: declare function、interface、交叉类型 &。

当重载比较多的时候,直接列出来还是比较麻烦的,这时候可以用类型编程来动态生成函数重载。

我们实现了联合转交叉,利用了函数参数的逆变性质,也就是当参数可能是多个类型时,会返回它们的交叉类型。

然后又利用分布式条件类型的性质,当传入联合类型时,会把类型单独传入做计算,最后把结果合并成联合类型。

利用这个实现了传入联合类型返回构造出的函数的联合类型,然后再结合联合转交叉就实现了函数重载的动态生成。

当你写重载写的太多的时候,不妨试一下用类型编程的方式动态生成吧!

Guess you like

Origin juejin.im/post/7085311864972214302