随着条件类型或变量元组类型等最新类型系统功能的出现,一种描述函数接口的技术已经淡出了人们的视野。函数重载。而这是有原因的。这两个特性都是为了处理常规函数重载的缺点而实现的。
请看这个直接来自TypeScript 4.0发布说明的连接例子。这是一个数组concat 函数。
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
如果要正确输入这样的函数,使其考虑到所有可能的边缘情况,我们最终会陷入重载的海洋。
// 7 overloads for an empty second array
function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
// 7 more for arr2 having one element
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];
// and so on, and so forth
而这只考虑到了最多有六个元素的数组。变量元组类型对这样的情况有很大帮助。
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
你可以很容易地看到如何将函数签名归结为它的要点,同时对所有可能出现的数组有足够的灵活性。返回值也映射到返回类型。没有额外的断言,TypeScript可以确保你返回的是正确的值。
这是一个类似于条件类型的情况。这个例子直接来自我的书。想想那些根据客户、文章或订单ID检索订单的软件。你可能想创建这样的东西。
function fetchOrder(customer: Customer): Order[]
function fetchOrder(product: Product): Order[]
function fetchOrder(orderId: number): Order
// the implementation
function fetchOrder(param: any): Order | Order[] {
//...
}
但这只是事实的一半。如果你最终遇到了模糊的类型,你不知道到底是只得到一个客户,还是只得到一个产品,怎么办。你需要照顾到所有可能的组合。
function fetchOrder(customer: Customer): Order[]
function fetchOrder(product: Product): Order[]
function fetchOrder(orderId: number): Order
function fetchOrder(orderId: Customer | Product): Order[]
function fetchOrder(orderId: Customer | number): Order | Order[]
function fetchOrder(orderId: number | Product): Order | Order[]
// the implementation
function fetchOrder(param: any): Order | Order[] {
//...
}
增加更多的可能性,你最终会有更多的组合。在这里,条件类型可以极大地减少你的函数签名。
type FetchParams = number | Customer | Product;
type FetchReturn<T> = T extends Customer ? Order[] :
T extends Product ? Order[] :
T extends number ? Order: never
function fetchOrder<T extends FetchParams>(params: T): FetchReturn<T> {
//...
}
由于条件类型分布在一个联合体中,FetchReturn 返回一个返回类型的联合体。
因此,有充分的理由使用这些技术而不是淹没在太多的函数重载中。这就产生了一个问题。我们还需要函数重载吗?
TL;DR:是的,我们需要函数重载。
这里有几个例子。
不同的函数形状#
函数重载仍然非常方便的一种情况是,如果你的函数变体有不同的参数列表。这意味着不仅参数(参数)本身可以有一些变化(这就是条件式和变体图元的奇妙之处),而且参数的数量和位置也是如此。
想象一下,一个搜索函数有两种不同的被调用方式。
- 用搜索查询来调用它。它返回一个你可以等待的Promise。
- 用搜索查询和一个回调调用它。在这种情况下,该函数不返回任何东西。
这可以用条件类型来完成,但非常不方便。
// => (1)
type SearchArguments =
// Argument list one: a query and a callback
[query: string, callback: (results: unknown[]) => void] |
// Argument list two:: just a query
[query: string];
// A conditional type picking either void or a Promise depending
// on the input => (2)
type ReturnSearch<T> = T extends [query: string] ? Promise<Array<unknown>> : void;
// the actual function => (3)
declare function search<T extends SearchArguments>(...args: T): ReturnSearch<T>;
// z is void
const z = search("omikron", (res) => {
})
// y is Promise<unknown>
const y = search("omikron")
下面是我们的做法。
- 我们使用元组类型来定义我们的参数列表。从TypeScript 4.0开始,我们可以给元组字段命名,就像我们对对象那样做。我们创建了一个联盟,因为我们的函数签名有两个不同的变体
ReturnSearch,根据参数列表的变体来选择返回类型。如果它只是一个字符串,就返回Promise,如果它有一个回调,就返回void。- 我们通过将一个泛型变量约束到
SearchArguments,来添加我们的类型,这样我们就可以正确地选择返回类型。
这是个很大的问题!而且它具有大量复杂的功能,我们喜欢在TypeScript的功能列表中看到。条件类型、泛型、泛型约束、元组类型、联合类型!我们得到了一些不错的自动完成功能,但它远没有一个简单的函数重载那么清晰。
function search(query: string): Promise<unknown[]>
function search(query: string, callback: (result: unknown[]) => void): void
// This is the implementation, it only concerns you
function search(query: string, callback?: (result: unknown[]) => void): void | Promise<unknown> {
// Implmeent
}
我们只在实现部分使用一个联合类型。其余的部分是非常明确和清晰的。我们知道我们的参数,我们知道期望得到什么回报。没有仪式,只有简单的类型。函数重载最好的部分是,实际的实现并不污染类型空间。你可以进行一轮任意运算,而不需要关心。
精确参数#
另一种情况是,当你需要精确的参数和它们的映射时,函数重载可以使很多事情变得简单。让我们来看看一个将事件应用到事件处理程序的函数。例如,我们有一个MouseEvent ,想用它来调用一个MouseEventHandler 。键盘事件也是如此,等等。如果我们使用条件和联合类型来映射事件和处理程序,我们最终可能会得到这样的结果。
// All the possible event handlers
type Handler =
MouseEventHandler<HTMLButtonElement> |
KeyboardEventHandler<HTMLButtonElement>;
// Map Handler to Event
type Ev<T> =
T extends MouseEventHandler<infer R> ? MouseEvent<R> :
T extends KeyboardEventHandler<infer R> ? KeyboardEvent<R> : never;
// Create a
function apply<T extends Handler>(handler: T, ev: Ev<T>): void {
handler(ev as any); // We need the assertion here
}
乍一看,这看起来不错。但如果你考虑到你需要跟踪的所有变体,这可能会有点麻烦。
不过,还有一个更大的问题。TypeScript处理所有可能的事件变体的方式导致了一个*意外的交叉*。这意味着在函数体中,TypeScript无法知道你传递的是哪种处理程序。因此,它也不能告诉我们得到的是哪种事件。所以TypeScript说,这个事件可以是两个。一个鼠标事件,和一个键盘事件。你需要传递能够处理这两种事件的处理程序。这不是我们打算让我们的函数工作的方式。
实际的错误信息是TS 2345:"键盘事件|MouseEvent<HTMLButtonElement, MouseEvent>"类型的参数不能分配给 "MouseEvent<HTMLButtonElement, MouseEvent> & KeyboardEvent "的参数。
这就是为什么我们需要一个as any 类型的断言。只是为了使它能够真正调用事件的处理程序。
所以,这个函数签名在很多情况下都是有效的。
declare const mouseHandler: MouseEventHandler<HTMLButtonElement>;
declare const mouseEv: MouseEvent<HTMLButtonElement>
declare const keyboardHandler: KeyboardEventHandler<HTMLButtonElement>;
declare const keyboardEv: KeyboardEvent<HTMLButtonElement>;
apply(mouseHandler, mouseEv); // yeah!
apply(keyboardHandler, keyboardEv) // cool!
apply(mouseHandler, keyboardEv) // 💥breaks like it should!
但是一旦有了歧义,事情就不像它应该做的那样了。
declare const mouseOrKeyboardHandler:
MouseEventHandler<HTMLButtonElement> |
KeyboardEventHandler<HTMLButtonElement>;;
// No wait, this can cause problems!
apply(mouseOrKeyboardHandler, mouseEv);
当mouseOrKeyboardHandler 是一个键盘处理程序时,我们不能合理地传递一个鼠标事件。等一下。这正是上面的TS2345错误所要告诉我们的我们只是把问题转移到了另一个地方,用一个 "任意"的断言让它沉默。哦,不!
明确的、准确的函数签名使一切变得更容易。映射变得更清晰,类型签名更容易理解,而且不需要条件式或联合式。
// Overload 1: MouseEventHandler and MouseEvent
function apply(
handler: MouseEventHandler<HTMLButtonElement>,
ev: MouseEvent<HTMLButtonElement>): void
// Overload 2: KeyboardEventHandler and KeyboardEvent
function apply(
handler: KeyboardEventHandler<HTMLButtonElement>,
ev: KeyboardEvent<HTMLButtonElement>): void
// The implementation. Fall back to any. This is not a type!
// TypeScript won't check for this line nor
// will it show in the autocomplete.
//This is just for you to implement your stuff.
function apply(handler: any, ev: any): void {
handler(ev);
}
函数重载帮助我们处理所有可能的情况。我们基本上可以确保没有模棱两可的类型。
apply(mouseHandler, mouseEv); // yeah!
apply(keyboardHandler, keyboardEv) // cool!
apply(mouseHandler, keyboardEv) // 💥 breaks like it should!
apply(mouseOrKeyboardHandler, mouseEv); // 💥 breaks like it should
对于实现,我们甚至可以使用任何。这不是TypeScript所看到的类型,这只是为你实现你的东西。既然你能确保不会遇到隐含歧义的情况,那么我们就可以依靠快乐的类型,不需要再费心了。
底线#
函数重载仍然是非常有用的,而且对于很多情况下来说,它是一种方式。它们更容易阅读,更容易编写,而且在很多情况下比我们用其他方法得到的东西更精确。
但这并不是非此即彼。如果你的情况需要,你可以愉快地混合和匹配条件语句和函数重载。