是时候表演真正的技术了 - TS 分身之术
阅读须知:本文示例的运行环境是 TypeScript 官网的 Playground,对应的编译器版本是 v3.8.3。
一、可爱又可恨的联合类型
由于 JavaScript 是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny
的配置项时,以上代码会提示以下错误信息:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
该信息告诉我们参数 x 和参数 y 隐式具有 any
类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add
函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number
联合类型,同时我们为该联合类型取个别名:
type Combinable = string | number;
在定义完 Combinable 联合类型后,我们来更新一下 add
函数:
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
为 add
函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add
函数就完美了么,我们来实际测试一下:
const result = add('Semlinker', ' Kakuqo');
result.split(' ');
在上面代码中,我们分别使用 'Semlinker'
和 ' Kakuqo'
这两个字符串作为参数调用 add 函数,并把调用结果保存到一个名为 result
的变量上,这时候我们想当然的认为此时 result 的变量的类型为 string,所以我们就可以正常调用字符串对象上的 split
方法。但这时 TypeScript 编译器又出现以下错误信息了:
Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.
很明显 Combinable
和 number
类型的对象上并不存在 split
属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript 提供的函数重载特性。
二、函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string
类型。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。
三、构造函数重载
在 TypeScript 类中构造函数是一种特殊的函数,用于构造指定类的对象。对于构造函数来说,它也是支持重载的,下面我们直接看个示例:
interface Shape {
x: number;
y: number;
height: number;
width: number;
}
class Square {
public x: number;
public y: number;
public height: number;
public width: number;
constructor();
constructor(obj: Shape);
constructor(obj?: any) {
this.x = obj?.x ?? 0;
this.y = obj?.y ?? 0;
this.height = obj?.height ?? 0;
this.width = obj?.width ?? 0;
}
}
在以上代码中,我们重载了 Square 类的构造函数,以支持不同的构造方式。接下来我们来验证一下不同构造方式:
1. 无参构造方式
let square1: Square;
square1 = new Square();
square1.x = 10;
square1.y = 50;
square1.height = 100;
square1.width = 100;
2. 使用 Shape 类型的对象进行构造
let squareConfig: Shape;
squareConfig = { x: 10, y: 50, height: 100, width: 100 };
let square2: Square;
square2 = new Square(squareConfig);
let square3: Square;
square3 = new Square({ x: 10, y: 50, height: 100, width: 100 });
四、特定重载签名
我们可以使用一个特定的签名来创建具有同样名称、参数数量但是有不同的返回类型的多个函数。为了创建一个特定签名,必须将函数的参数类型指定为一个字符串。这个字符串用于定义哪个函数重载被调用:
// typescript/lib/lib.dom.d.ts
createEvent(eventInterface: "KeyboardEvent"): KeyboardEvent; // 特定重载签名
createEvent(eventInterface: "MouseEvent"): MouseEvent; // 特定重载签名
createEvent(eventInterface: "TouchEvent"): TouchEvent; // 特定重载签名
createEvent(eventInterface: string): Event; // 非特定重载签名
在这个例子中,我们为函数 createEvent 声明了三个特定重载签名和一个非特定重载签名。当在一个对象中声明特定签名时,这个对象中必须被赋予至少一个非特定重载签名。且在编写重载签名时,必须在最后列出非重载签名。
五、参考资源
tslang.cn - functions typescript-function-overloads TypeScript 中的方法重载