函数与泛型
函数(function)
两种JavaScript中的函数:
// Named function function add(x, y) { return x + y; } // Anonymous function let myAdd = function(x, y) { return x + y; };
在JavaScript里,函数可以使用函数体外部的变量。 至于为什么可以这样做以及其中的利弊超出了本文的范围, 但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。
let z = 100; function addToZ(x, y) { return x + y + z; }
函数类型
为函数定义类型
让我们为上面那个函数添加类型:
function add(x: number, y: number): number { return x + y; } let myAdd = function(x: number, y: number): number { return x + y; };
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
书写完整函数类型
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。
let myAdd: (x:number, y:number) => number = function(x: number, y: number): number { return x + y; };
函数类型包含两部分:参数类型和返回值类型。
当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。
这个名字只是为了增加可读性。 我们也可以这么写:
let myAdd: (baseValue: number, increment: number) => number = function(x: number, y: number): number { return x + y; };
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,
我们在函数和返回值类型之前使用=>
符号,使之清晰明了。 如之前提到的,
返回值类型是函数类型的必要部分,如果函数没有返回任何值,
你也必须指定返回值类型为void
而不能留空。
函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
推断类型
尝试这个例子的时候, 你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话, 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; };
这叫做「按上下文归类」,是类型推论的一种。 它帮助我们更好地为程序指定类型。
可选参数和默认参数
TypeScript里的每个函数参数都是必须的。
这不是指不能传递null
或undefined
作为参数,
而是说编译器检查用户是否为每个参数都传入了值。
编译器还会假设只有这些参数会被传递进函数。
简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
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
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,
它的值就是undefined
。
在TypeScript里我们可以在参数名旁使用?
实现可选参数的功能。 比如,
// 参数lastName是可选的 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
可选参数必须跟在必须参数后面。 如果上例我们想让firstName
是可选的,
那么就必须调整它们的位置。
可以为参数提供一个默认值,把lastName
的默认值设置为"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
值来获得默认值。
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 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
TypeScript是JavaScript的超集,
也需要弄清this
工作机制并且当有bug的时候能够找出错误所在。
TypeScript能通知你错误地使用了this
的地方。
如果你想了解JavaScript里的this是如何工作的,那么首先阅读Yehuda Katz写的 文章详细的阐述了this的内部工作原理: Understanding JavaScript Function Invocation and “this”
this
和箭头函数
JavaScript里,this的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点, 但是你需要花点时间弄清楚函数调用的上下文是什么, 尤其是在返回一个函数或将函数当做参数传递的时候。
下面看一个例子:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { 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);
可以看到createCardPicker
是个函数,并且它又返回了一个函数。
如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。
因为createCardPicker
返回的函数里的this
被设置成了ewindow
而不是deck
对象。
因为我们只是独立的调用了cardPicker()
。
顶级的非方法式调用会将this
视为window
。
(注意:在严格模式下,this
为undefined
而不是window
)。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this
。 这样的话,
无论之后怎么使用它,都会引用绑定的deck
对象。
我们需要改变函数表达式来使用ECMAScript 6箭头语法。
箭头函数能保存函数创建时的this
值,而不是调用时的值:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // NOTE: the line below is now an arrow function, // allowing us to capture 'this' right here 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会警告你犯了一个错误,
如果你给编译器设置了--noImplicitThis
标记。
它会指出this.suits[pickedSuit]
里的this
的类型为any
。
this
参数
不幸的是,this.suits[pickedSuit]
的类型依旧为any
。
这是因为this
来自对象字面量里的函数表达式。 修改的方法是,
提供一个显式的this
参数。this
参数是个假的参数,它出现在参数列表的最前面:
function f(this: void) { // make sure `this` is unusable in this standalone function }
让我们往例子里添加一些接口,Card
和Deck
,让类型重用能够变得清晰简单些:
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
参数
当你将一个函数传递到某个库函数里在稍后被调用时,
你可能也见到过回调函数里的this
会报错。 因为当回调函数被调用时,
它会被当成一个普通函数调用,this
将为undefined
。
稍做改动,你就可以通过this
参数来避免错误。 首先,库函数的作者要指定this
的类型:
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void; }
this: void
意味着addClickListener
期望onclick
是一个函数且它不需要一个this
类型。 然后,为调用代码里的this添加类型注解:
class Handler { info: string; onClickBad(this: Handler, e: Event) { // oops, used this here. using this callback would crash at runtime this.info = e.message; } } let h = new Handler(); uiElement.addClickListener(h.onClickBad); // error!
指定了this
类型后,你显式声明onClickBad
必须在Handler
的实例上调用。
然后TypeScript会检测到addClickListener
要求函数带有this: void
。
改变this
类型来修复这个错误:
class Handler { info: string; onClickGood(this: void, e: Event) { // can't use this here because it's of type void! console.log('clicked!'); } } let h = new Handler(); uiElement.addClickListener(h.onClickGood);
因为onClickGood
指定了this
类型为void
,
因此传递addClickListener
是合法的。 当然了,这也意味着不能使用this.info
.
如果你两者都想要,你不得不使用箭头函数了:
class Handler { info: string; onClickGood = (e: Event) => { this.info = e.message } }
这是可行的因为箭头函数不会捕获this
,
所以你总是可以把它们传给期望this: void
的函数。
缺点是每个Handler对象都会创建一个箭头函数。
另一方面,方法只会被创建一次,添加到Handler
的原型链上。
它们在不同Handler
对象间是共享的。
重载
JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。
let suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card 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);
pickCard
方法根据传入参数的不同会返回两种不同的类型。
如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。
如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。
编译器会根据这个列表去处理函数的调用。 下面我们来重载pickCard
函数。
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 { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card 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);
这样改变后,重载的pickCard
函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:
一个是接收对象另一个接收数字。 以其它参数调用pickCard
会产生错误。
泛型(generic)
泛型基础
下面来创建第一个使用泛型的例子:identity
函数。 这个函数会返回任何传入它的值。
你可以把这个函数当成是echo命令。
不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number { return arg; }
或者,我们使用any类型来定义函数:
function identity(arg: any): any { return arg; }
这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T { return arg; }
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,
使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 –
即编译器会根据传入的参数自动地帮助我们确定T
的类型:
let output = identity("myString"); // type of output will be 'string'
编译器可以查看myString
的值,然后把T设置为它的类型。
类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,
只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
使用泛型变量
使用泛型创建泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。
看下之前例子:
function identity<T>(arg: T): T { return arg; }
如果我们想同时打印出arg
的长度。 我们很可能会这样做:
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg; }
如果这么做,编译器会报错说我们使用了arg
的.length
属性,
但是没有地方指明arg
具有这个属性。
现在假设我们想操作T
类型的数组而不直接是T
。所以.length
属性是应该存在的。
我们可以像创建其它数组一样创建这个数组:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg; }
如果我们传入数字数组,将返回一个数字数组,因为此时T
的的类型为number
,
而参数arg
的完整类型是数字数组。
这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg; }
泛型类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面, 像函数声明一样:
function identity<T>(arg: T): T { return arg; } let myIdentity: <T>(arg: T) => T = identity; // 我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。 let myIdentity: <U>(arg: U) => U = identity; // 我们还可以使用带有调用签名的对象字面量来定义泛型函数: let myIdentity: {<T>(arg: T): T} = identity;
这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:
interface GenericIdentityFn { <T>(arg: T): T; } function identity<T>(arg: T): T { return arg; } let myIdentity: GenericIdentityFn = identity;
一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。
这样我们就能清楚的知道使用的具体是哪个泛型类型
(比如:Dictionary<string>
而不只是Dictionary
)。
这样接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<T> { (arg: T): T; } function identity<T>(arg: T): T { return arg; } let myIdentity: GenericIdentityFn<number> = identity;
注意,我们的示例做了少许改动。 不再描述泛型函数,
而是把非泛型函数签名作为泛型类型一部分。
当我们使用GenericIdentityFn
的时候,
还得传入一个类型参数来指定泛型类型(这里是:number
),
锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,
理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。
除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。
泛型类
泛型类看上去与泛型接口差不多。 泛型类使用(<>
)括起泛型类型,跟在类名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function(x, y) { return x + y; };
GenericNumber
类的使用是十分直观的,并且你可能已经注意到了,
没有什么去限制它只能使用number
类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; }; alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,直接把泛型类型放在类后面, 可以帮助我们确认类的所有属性都在使用相同的类型。
我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,
并且我们知道这组值具有什么样的属性。 在loggingIdentity
例子中,
我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有length
属性,
所以就报错了。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg; }
相比于操作any
所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。
只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。
为此,我们定义一个接口来描述约束条件。 创建一个包含.length
属性的接口,
使用这个接口和extends
关键字还实现约束:
interface Lengthwise { length: number; } function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg; }
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,
现在我们想要用属性名从对象里获取这个属性。
并且我们想要确保这个属性存在于对象obj
上,因此我们需要在这两个类型之间使用约束。
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; } let x = { a: 1, b: 2, c: 3, d: 4 }; getProperty(x, "a"); // okay getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
在泛型里使用类类型
在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
function create<T>(c: {new(): T; }): T { return new c(); }
一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。
class BeeKeeper { hasMask: boolean; } class ZooKeeper { nametag: string; } class Animal { numLegs: number; } class Bee extends Animal { keeper: BeeKeeper; } class Lion extends Animal { keeper: ZooKeeper; } function createInstance<A extends Animal>(c: new () => A): A { return new c(); } createInstance(Lion).keeper.nametag; // typechecks! createInstance(Bee).keeper.hasMask; // typechecks!
枚举(enum)
定义枚举
使用枚举我们可以定义一些有名字的数字常量。 枚举通过enum
关键字来定义。
enum Direction { Up = 1, Down, Left, Right }
一个枚举类型可以包含零个或多个枚举成员。 枚举成员具有一个数字值, 它可以是常数或是计算得出的值 当满足如下条件时,枚举成员被当作是常数:
- 不具有初始化函数并且之前的枚举成员是常数。 在这种情况下, 当前枚举成员的值为上一个枚举成员的值加1。 但第一个枚举元素是个例外。 如果它没有初始化方法,那么它的初始值为0。
-
枚举成员使用常数枚举表达式初始化。 常数枚举表达式是TypeScript表达式的子集,
它可以在编译阶段求值。 当一个表达式满足下面条件之一时,
它就是一个常数枚举表达式:
- 数字字面量
- 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的) 如果这个成员是在同一个枚举类型中定义的,可以使用非限定名来引用。
- 带括号的常数枚举表达式
-
+
,-
,~
一元运算符应用于常数枚举表达式 -
+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
二元运算符, 常数枚举表达式做为其一个操作对象。 若常数枚举表达式求值后为NaN
或Infinity
, 则会在编译阶段报错。
所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write, // computed member G = "123".length }
枚举是在运行时真正存在的一个对象。 其中一个原因是因为这样可以从枚举值到枚举名进行反向映射。
enum Enum { A } let a = Enum.A; let nameOfA = Enum[a]; // "A"
编译成:
var Enum; (function (Enum) { Enum[Enum["A"] = 0] = "A"; })(Enum || (Enum = {})); var a = Enum.A; var nameOfA = Enum[a]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含双向映射
(name -> value
)和(value -> name
)。
引用枚举成员总会生成一次属性访问并且永远不会内联。
在大多数情况下这是很好的并且正确的解决方案。 然而有时候需求却比较严格。
当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。
常数枚举是在enum
关键字前使用const
修饰符。
const enum Enum { A = 1, B = A * 2 }
常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员。
const enum Directions { Up, Down, Left, Right } let directions = [ Directions.Up, Directions.Down, Directions.Left, Directions.Right ]
生成后的代码为:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
外部枚举
外部枚举用来描述已经存在的枚举类型的形状。
declare enum Enum { A = 1, B, C = 2 }
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里, 没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言, 没有初始化方法时被当做需要经过计算的。