文章目录
TypeScript 关于函数
函数是任何应用的基本构建块,无论它们是本地函数、从另一个模块导入的函数,还是类中的方法。它们也是值,就像其他值一样,TypeScript 有很多方法来描述如何调用函数。让我们学习如何编写描述函数的类型。
函数类型限制
具名函数和匿名函数
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };
为函数添加类型
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
一个完整的函数类型
myAdd 是一个函数,对其进行限制,其中 => 前面是参数,=> 后面是返回值。参数接收两个 number 类型的,参数名可以定义为任意参数名。返回值是 number 类型的,值由一个函数获得,该函数入参和返回值都是 number 类型。
// 给出了完整了类型限制
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
函数类型推断
// 给出了完整的类型限制,不会触发类型推断
let myAdd = function(x: number, y: number): number { return x + y; };
// x,y没有限制类型,但因为返回值是 number 类型的,所以 x,y 自动推断为 number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
函数类型表达式
描述函数的最简单方法是使用函数类型表达式。这些类型在语法上类似于箭头函数。
语法 (a: string) => void 的意思是“一个带有一个参数、名为 a、类型为 string、没有返回值的函数”。就像函数声明一样,如果未指定参数类型,则隐式为 any。
❗❗❗请注意,参数名称是必需的。函数类型 (string) => void 的意思是“一个带有名为 string、类型为 any 的参数的函数”!
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
也可以使用类型别名命名函数类型。
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
调用签名
在 JavaScript 中,函数除了可调用之外还可以具有属性。但是,函数类型表达式语法不允许声明属性。如果我们想用属性描述可调用的东西,我们可以在对象类型中编写调用签名。
❗❗❗请注意,与函数类型表达式相比,语法略有不同。在参数列表和返回类型之间使用 : 而不是 =>。
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
function myFunc(someArg: number) {
return someArg > 3;
}
myFunc.description = "default description";
doSomething(myFunc);
构造签名
JavaScript 函数也可以使用 new 运算符调用。TypeScript 将它们称为构造函数,因为它们通常会创建一个新对象。你可以通过在调用签名前添加 new 关键字来编写构造签名。
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
一些对象,比如 JavaScript 的 Date 对象,可以在有或没有 new 的情况下调用。你可以任意组合相同类型的调用和构造签名。
interface CallOrConstruct {
(n?: number): string;
new (s: string): Date;
}
可选参数和默认参数
只要定义了参数就必须传值,即实参和形参个数要一致,否则报错。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
如果不一定传某个参数,可以用可选参数接收。可选参数必须放在最后。
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
可以用默认参数去实现可选参数功能。当用户没有传递这个参数或传递的值是undefined 时触发默认参数赋值。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right
与可选参数不同的是,默认参数可以不放在最后,但如果想要触发默认参数赋值,必须传 undefined。
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
回调中的可选参数
一旦我们了解了可选参数,很容易写出下面的问题代码。
规则:为回调编写函数类型时,切勿编写可选参数,除非你打算在不传递该参数的情况下调用该函数
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i]);
}
}
myForEach([1, 2, 3], (a) => console.log(a)); // ok
myForEach([1, 2, 3], (a, i) => console.log(a, i)); // ok
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed()); // 报错 'i' is possibly 'undefined'.
});
剩余参数
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
参数解构
详见 ECMAScript 常用语法之解构赋值-函数参数的解构赋值
函数重载
重载签名和实现签名
下面示例前两个没有函数体的 len 是重载签名,一般重载签名至少有两个,不然就够不上重载这一说。最后一个有函数体的 len 叫实现签名。
注意规则❗❗❗
- 从外部看不到实现的签名。在编写重载函数时,你应该始终在函数实现之上有两个或多个签名。
- 实现签名必须兼容重载签名
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]); // 报错
最后一个调用报错,因为我们可以用字符串或数组调用它。但是,我们不能使用可能是字符串或数组的值来调用它,因为 TypeScript 只能将函数调用解析为单个重载。
对比联合类型
上面重载函数我们可以用联合类型去实现相同的功能。
function len(x: any[] | string) {
return x.length;
}
尽可能使用联合类型的参数而不是重载
函数中的 this
TypeScript 将通过代码流分析推断函数中的 this 应该是什么,例如:
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
TypeScript 理解函数 user.becomeAdmin 有一个对应的 this,它是外部对象 user。很多情况下就够用了,但也有很多情况下,你需要更多的控制 this 代表什么对象。JavaScript 规范规定你不能有一个名为 this 的参数,因此 TypeScript 使用该语法空间让你在函数体中声明 this 的类型。
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
这种模式在回调风格的 API 中很常见,其中另一个对象通常控制何时调用你的函数。请注意,你需要使用 function 而不是箭头函数来获得此行为:
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(() => this.admin);
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.