函数作为js一等公民,在js中占有重要地位。ts为函数添加了额外的功能,使之更容易被使用。
ts中有Function类型,但使用Function,在调用函数时得不到很好的类型检测和提示(就这类似于之前提到的object和接口类型)
函数类型声明
在ts中函数类型声明有两种方式:函数声明和函数表达式(与js一致)。不同是需要指定参数和返回值的类型。
// 函数声明
function add(x: number, y: number): number {
return x + y;
}
// 函数表示式
const myAdd = function (x: number, y: number): number {
return x + y;
};
函数表示式也可以写成:
const myAdd: (x: number, y: number) => number = function (x, y) {
return x + y;
};
在函数表示式指定类型后,ts会自动推断实际参数的x
和y
的类型是number
,返回值是string
。
并且函数类型声明(x: number, y: number) => number
中参数名不必和所赋值的函数参数名相同,只要保证各个参数类型兼容即可。
const myAdd: (base: number, increment: number) => number = function (x, y) {
return x + y;
};
// 参数类型不兼容,将报错
// Type '(x: string, y: number) => string' is not assignable to type '(base: string, increment: number) => number'.
// Type 'string' is not assignable to type 'number'.
const myAdd: (base: string, increment: number) => number = function (x, y) {
return x + y;
};
实际上,我们并不一定要为函数返回值显示指定类型,ts会根据参数类型,和return
语句推断出返回值类型。这就是ts强大类型推断功能。
// 返回值类型会被推断为number
function add(x: number, y: number) {
return x + y;
}
// 类型不兼容,将报错
// Type 'number' is not assignable to type 'string'
const str:string=add(1,2)
可选参数
默认情况下,在调用函数时,ts会对每个参数进行检查,保证函数调用时的实参和函数声明时的形参的个数和类型都匹配。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
const result = buildName("Bob", "Adams");
// Expected 2 arguments, but got 1
const result2 = buildName("Bob"); // error, too few parameters
// Expected 2 arguments, but got 3
const result3 = buildName("Bob", "Adams", "Sr.");
ts为函数提供了可选参数,使用形式和interface
的可选属性使用形式相似,在参数名后加?
。
function buildName(firstName: string, lastName?: string) {
return firstName + " " + lastName;
}
const result = buildName("Bob");
可选参数必须排在必需参数之后,否则会报错:
// 报错
// A required parameter cannot follow an optional parameter
function buildName(firstName?: string, lastName: string) {
return firstName + " " + lastName;
}
默认参数
我们可以为参数提供默认值,如果该参数被传入undefined
,将使用默认值代替。同时,如果默认参数在必需参数之后,则会被ts自动识别为可选参数。
// c将会被标记为带默认值的可选参数
function buildLetter(a: string = "a", b: string, c: string = "c") {
return a + b + c;
}
// a=First, b=Second, c=Third
buildLetter("First", "Second", "Third");
// a=First, b=Second, c=c
buildLetter("First", "Second");
// a=a, b=Srcond, c=c
buildLetter(undefined, "Second");
剩余参数
不管是可选参数还是默认参数,它们都只针对一个参数。而通过ES6解构赋值可以实现对函数剩余参数的收集。剩余参数会被ts标识为一组数量不定(从0到任意多个)的可选参数,所以剩余参数是数组类型,并且必须放在必需参数之后。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
const names = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
This参数
由于js的this
指向在函数调用时动态变化的,使得它能实现很多强大且灵活的功能。但是带来的代价是:判断this
指向变得令人头疼。
const user = {
devices: ["mobile", "laptop", "desktop"],
createDevicePicker: function () {
return function () {
return this.devices.join(",");
};
},
};
const picker = user.createDevicePicker();
console.log(picker());
如上面这段代码,我们期待输出的是mobile,laptop,desktop
字符串,但是实际结果是一个Cannot read property 'devices' of undefined
错误。
这是因为createDevicePicker
所返回的新函数被真正调用时,实际上内部this
指向的是undefined
(非严格模式下指向全局变量)。
通常建议在配置文件中打开noImplicitThis
,ts会对具有any
类型的this
进行警告。

上面的例子,如果使用ES6的箭头函数可以解决this
指向的问题,同时ts也能正确推断出this
类型。
const user = {
devices: ["mobile", "laptop", "desktop"],
createDevicePicker: function () {
return () => {
return this.devices.join(",");
};
},
};
const picker = user.createDevicePicker();
console.log(picker());

本质上这还是js所带来的问题,与ts无关,对于
this
具体指向请查看其它相关文档,本节不再赘述。
ts允许我们显示声明this
的指向,通过使用this
作为函数的第一个参数。this
参数只是用来做类型约束(编译时),实际this
指向不一定是如此,依赖于具体实现(运行时)
function f(this: void) {
// 不能在该函数内使用this变量
}
试想一种场景:我们有一个ui接口,它有一个onClick
的方法用来收集回调方法,并且我们期望回调方法都不需要使用this
访问元素内部数据:
interface UIElement {
message: string;
onClick: (onClick: (this: void, message: string) => void) => void;
}
const ulElement: UIElement = {
message: "click message",
onClick(callback) {
callback(this.message);
},
};
function callback(this: void, message: string) {
console.log(message);
}
ulElement.onClick(callback);
// 如果callback指定了不兼容的this将会报错
// The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'UIElement'
function cb(this:UIElement,message:string) {
}
ulElement.onClick(cb);
如果我们期望回调函数需要访问元素内部数据,可以这样实现:
interface UIElement {
message: string;
onClick: (onClick: (this: UIElement) => void) => void;
}
const ulElement: UIElement = {
message: "click message",
onClick(callback) {
callback.call(this);
},
};
function callback(this: UIElement) {
console.log(this.message);
}
ulElement.onClick(callback);
重载
重载在其他面向对象的语言中是十分常见的功能,但是js不是那么容易实现此功能。
比如,我们现在有一个需求,有一个名为calculateLength
的函数,它有两个参数:
- 如果两个参数类型都是
string
,则返回字符串长度相加之和 - 如果两个参数类型都是
number
,则返回两个数值相加之和 - 没有其他组合形式
借助联合类型,我们可以按照如下方式实现:
function calculateLength(a: string | number, b: string | number): number {
if (typeof a === "string" && typeof b === "string") {
return a.length + b.length;
} else if (typeof a === "number" && typeof b === "number") {
return a + b;
} else {
return 0;
}
}
但是我们实际调用函数时,以下方式都合法:
calculateLength("str1", "str2");
calculateLength(1, 3);
// 不符合规范的形式
calculateLength(1, "str2");
calculateLength("str1", 3);
可以看出只靠联合类型,无法精确表达需求。此时我们需要利用重载对参数组合进行限制:
function calculateLength(a: string, b: string): number;
function calculateLength(a: number, b: number): number;
function calculateLength(a: string | number, b: string | number): number {
if (typeof a === "string" && typeof b === "string") {
return a.length + b.length;
} else if (typeof a === "number" && typeof b === "number") {
return a + b;
} else {
return 0;
}
}
calculateLength("str1", "str2");
calculateLength(1, 3);
// 此时对于不符合规范的参数组合会报错
// No overload matches this call.
calculateLength(1, "str2");
calculateLength("str1", 3);
可以看到,我们重复声明了多次calculateLength
函数,除了最后一次是具体的函数实现,前面都只是函数类型声明。
ts在判断函数调用是否符合规范时,会按照重载声明的顺序从前向后依次匹配,直至找到为止。如果重载声明具有包含关系,需要优先把最精确的声明放在前面。
在上面的例子中,如果重载声明为:
function calculateLength(a: any, b: any): number;
function calculateLength(a: string, b: string): number;
function calculateLength(a: number, b: number): number;
任何函数调用都会被匹配到第一个函数声明,所有实参类型组合都会被认为合法。
也可使用接口表示重载:
interface CalculateLength {
(a: string, b: string): number;
(a: number, b: number): number;
}
const fn: CalculateLength = (
a: string | number,
b: string | number
): number => {
if (typeof a === "string" && typeof b === "string") {
return a.length + b.length;
} else if (typeof a === "number" && typeof b === "number") {
return a + b;
} else {
return 0;
}
};
void还是undefined
之前我们在介绍void
类型时,提到如果函数没有显式的返回值,则返回类型是void
。在js中,如果函数没有显式的返回值,会默认返回undefined
。那可不可以用undefined
作为这种情况的返回值呢?

可以看到此时ts会报错,它要求如果返回值类型不是any
或者void
,函数必须有一个显式的返回值。所以如果要返回值为undefined
合法,必须在函数末尾加return undefined
。
这也可以解释为什么开启strictNullChecks
后,undefined
依旧可以赋值给void
。