原文地址 www.tslang.cn
函数类型
为函数定义类型
让我们为上面那个函数添加类型:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript 能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
书写完整函数类型
函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用 ( =>
) 符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void
而不能留空。
推断类型
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript 编译器会自动识别出类型:
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
这叫做 “按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。
可选参数和默认参数
JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined。 在 TypeScript 里我们可以在参数名旁使用 ?
实现可选参数的功能。 比如,我们想让 last name 是可选的:
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
可选参数必须跟在必须参数后面。 如果上例我们想让 first name 是可选的,那么就必须调整它们的位置,把 first name 放在后面。
注释:所以函数的参数使用一个对象接收会更容易扩展,而不用调整参数的位置。
在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined
时。 它们叫做有默认初始化值的参数。 让我们修改上例,把 last name 的默认值设置为"Smith"
。
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
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) {
// ...
}
和
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享同样的类型(firstName: string, lastName?: string) => string
。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。 例如,我们重写最后一个例子,让 firstName
是带默认值的参数:
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"
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments
来访问所有传入的参数。
在 TypeScript 里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
this
和箭头函数
在 JS 中顶级的非方法式调用会将 this
视为window
。 (注意:在严格模式下, this
为undefined
而不是window
)。
this
参数
注释:这里的讲解并不清楚。这里显示的 this 参数是用来限定调用这个方法的对象的类型。在 TS 中,顶级的非方法式调用时 this 视为 void 类型。
不幸的是,this.suits[pickedSuit]
的类型依旧为any
。 这是因为 this
来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this
参数。 this
参数是个假的参数,它出现在参数列表的最前面:
function f(this: void) {
// make sure `this` is unusable in this standalone function
}
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
现在 TypeScript 知道createCardPicker
期望在某个Deck
对象上调用。 也就是说 this
是Deck
类型的,而非any
,因此--noImplicitThis
不会报错了。
this
参数在回调函数里
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
重载
注释:重载只在函数调用时被用于解析,在定义函数时不起作用。即:下例中最后的函数实现中的参数 x 需要单独定义类型,它不会自动获取重载中关于 x 的类型定义。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
为了让编译器能够选择正确的检查类型,它与 JavaScript 里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard
会产生错误。
注释:使用接口实现重载
interface Fun{ (a: string): string; (a: number): number; }
或
type A = (a: string) => string type B = (b:number) => number const abc: A & B = a => a;
——————————————————————————————————————————
注释:以下内容来源于 TS 3.1 - 类型兼容性
比较两个函数
注释:一个函数的类型定义是用来给外部调用时使用的,所以在实现中参数要求在数量上要不大于类型。因为在调用时,即使传入较多的参数,这部分参数会被函数默默抛弃
注释:下例中 x 能够赋值给 y,那是因为 x 要求的参数数量比 y 少,调用时不会缺少必要参数。
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要查看x
是否能赋值给y
,首先看它们的参数列表。 x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x
的每个参数在y
中都能找到对应的参数,所以允许赋值。
第二个赋值错误,因为y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
你可能会疑惑为什么允许忽略
参数,像例子y = x
中那样。 原因是忽略额外的参数在 JavaScript 里是很常见的。 例如,Array#forEach
给回调函数传 3 个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:
let items = [1, 2, 3];
// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));
下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error, because x() lacks a location property
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
注释:源函数是指
x = y
中的y
函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多 JavaScript 里的常见模式。例如:
注释:这句话不能理解,请参考上一条注释关于函数类型是否兼容的说明
注释:下例标明,就函数的某个参数而言,实现的类型范围应该大于或等于函数类型定义中的范围
enum EventType { Mouse, Keyboard }
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// Unsound, but useful and common
// 注释:这是个错误示范,MouseEvent 范围小于 Event,所以下面的这个函数实例不能传给 listenEvent
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
// 注释:这是个错误
listenEvent(EventType.Mouse, (e: number) => console.log(e));
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
注释:目标类型拥有可选参数时,实现部分可有可没有,因为实现部分不接收这些参数也不会引起错误。当实现有可选参数时,目标类型可有可没有,因为实现部分必须保证可选参数未传入时的逻辑正确。
注释:下面这个例子中 invokeLater 的 callback 的类型定义并不严谨,容易导致回调出错。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
。
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。