本节介绍ts的函数及泛型的相关内容,包括函数的声明格式及泛型的相关知识。
函数是js程序的基础,可以实现抽象层/模拟类/信息隐藏和模块,ts中已经支持类/命名空间和模块,但函数是主要定义行为的地方,ts为js函数添加了额外的功能。
可以创建带有名字的函数或匿名函数,实例如下:
function add(x, y) {
return x + y;
}
let myAdd = function(x, y) { return x + y; };
函数可以使用函数体外部的变量,如:
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
可以给每个参数添加类型之后再为函数本身添加返回值类型,ts能够根据返回语句自动推断出返回值类型,示例如下:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
函数的完整类型定义如下:
let myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
函数类型包含两部分:参数类型和返回值类型,使用参数列表的形式写出参数类型,为每个参数指定名字和类型,只要参数类型是匹配的就可以,参数名不一定一致。返回值部分的类型使用=>符号声明,返回值类型是函数类型的必要部分,若没有任何返回值,则可指定为void,但是不能留空。
赋值语句的时候一边指定了类型但是另一边没有类型的话,ts编译器会自动识别出类型,叫做按上下文归类,是类型推论的一种:
// 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; };
ts中每个函数参数都是必须的,编译器会检查用户是否为每个参数都传入了值,还会假设只有这些参数会被传递进函数,即传递给一个函数的参数个数必须与函数期望的参数个数一致。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // 报错,缺少参数
let result2 = buildName("Bob", "Adams", "Sr."); //报错,多了参数
let result3 = buildName("Bob", "Adams"); // ah, just right
js函数中的每个参数都是可选的,可传可不传,没传参数的时候,它的值就是undefined,在ts中可以在参数名旁边使用?实现可选参数的功能,如:
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."); // 错误,多了参数
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."); // 异常,多个参数
let result4 = buildName("Bob", "Adams"); // ah, just right
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略,即可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName = "Smith") {
// ...
}
带默认值的参数不需要放在必须参数的后面,如果带默认值的参数出现在必须参数的前面,用户必须明确的传入undefined值来获取默认值,如:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); //异常,少参数
let result2 = buildName("Bob", "Adams", "Sr."); // 异常,多了参数
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
必要参数/默认参数和可选参数都表示某一个参数,若不知道有多少个参数传递的时候,可以使用arguments来访问所有传入的参数。ts中可以将所有参数收集到一个变量里:
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;
js中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函数返回的函数里的this被设置成了window而不是deck对象,因为独立调用了cardPicker,此时调用会将this视为window,严格模式下this为undefined而不是window。
若要正确的使用,可以在函数返回的时候就绑好正确的this,可以使用箭头函数,箭头函数会保存函数创建时的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);
上述代码中如果给编译器设置了–noImplicitThis标记。 它会指出this.suits[pickedSuit]里的this的类型为any。若需要明确具体的类型,可以提供一个显示的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);
上述示例中Deck 接口的createCardPicker指定了this的类型,则调用时候this的类型就是Deck的,而不是any,此时–noImplicitThis就不会报错了。
有时候在回调函数中this也会报错,因为当函数被当作参数传递,并作为回调函数时,当回调被调用的时候,会被当作一个普通函数调用,this将为undefined,可以通过this参数来避免错误,如下:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void表示addClickListener期望onclick是一个不需要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的实例上调用,ts会检测到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,所以可以进行传递,但每个Handler对象都会创建一个箭头函数,方法指挥被创建一次,添加到Handler的原型链上,在不同的Handler对象间是共享的。
js中函数传入不同的参数可返回不同类型的数据,如下:
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方法根据传入参数的不同会返回两种不同的类型,可以对函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用,示例如下:
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函数在调用的时候会进行正确的类型检查,处理时会查找重载列表,尝试使用第一个重载定义,如果匹配则使用,因此使用的时候最好把精确的定义放在最前面,示例中function pickCard(x): any并不是重载列表的一部分,因此示例中只有两个重载,一个是接受对象另一个是接受数字,其他的参数将会产生错误。
工程使用过程中,需要考虑可重用性,不仅能够支持当前的数据类型,也可以支持未来的数据类型。可以使用泛型来创建可重用的组件,让一个组件可支持多种类型的数据,示例如下:
function identity<T>(arg: T): T {
return arg;
}
  也可以使用any来定义函数,如:
function identity(arg: any): any {
return arg;
}
虽然使用any后函数能接受任何类型的参数,但是却丢失了一部分信息,即传入的类型和传出的类型应该是相同的,但是实际不确定具体的类型,使用了泛型后就能确保传入的参数和传出的值类型是一致的。泛型不会丢失信息,定义了泛型函数之后,有两种调用方式:
第一种,可传入所有的参数,包含类型参数,明确的指定了T是string类型,并作为参数传给函数,使用<>进行处理,如下:
let output = identity<string>("myString"); // type of output will be 'string'
第二种,可使用类型推导,让编译器自动根据传递的参数类型确定具体的类型,此时没必须要使用<>来明确的传入类型,如下:
let output = identity("myString"); // type of output will be 'string'
使用泛型创建泛型函数时,编译器要求函数体必须正确的使用,必须把参数当作任意或所有类型,示例如下:
function identity<T>(arg: T): T {
return arg;
}
如果想获取arg的长度,调用arg.length,编译器将会报错,因为没有地方指明arg具有length属性,此处的变量代表的时任意类型。也可以传入泛型指定的数组,如下:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
也可以使用如下方式定义:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
可使用V和W作为类型变量来表示任何字母都可以,名称没有具体的含义,JS中不支持泛型,所以在编译后无泛型相关的代码,泛型纯粹是为了编译时进行类型的校验,确保类型的安全抽象。使用方式:
interface Identities<V, W> {
id1: V,
id2: W
}
泛型类与泛型接口类型,使用<>指定泛型类型,跟在类名后面。使用如下:
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; };
与接口一样,直接把泛型类型放到类的后面,可以帮助确认类的所有属性都在使用相同的类型。
注意:泛型指定的是类的实例部分,所有类的静态部分不能使用泛型类型。
上述例子中使用参数arg的length属性时因为参数的类型时任意类型,所以会报错,这时可以使用泛型约束确保参数都包含length属性,这样既可以在函数中访问length属性了。示例如下:
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});
可以声明一个类型参数,且它被另一个类型参数所约束,比如:
function find<T, U extends Findable<T>>(n: T, s: U) {
// ...
}
find (giraffe, myAnimals);
在 TypeScript 使用泛型创建工厂函数时,需要引用构造函数的类类型,比如:
function create<T>(c: {new(): T; }): T {
return new c();
}
使用枚举可以定义有名字的数字常量,通过enum关键字进行定义:
enum Direction {
Up = 1,
Down,
Left,
Right
}
一个枚举类型可以包含零个或多个枚举成员,枚举成员具有一个数字值,可以是常数或计算得出的值,常数时需要满足一下条件:
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[Enum.A]; // "A"
编译后,枚举类型被编译成一个对象,它包含双向映射(name->value)和(value->name)。引用枚举成员总会生成一次属性访问并且永远不会内联,当访问枚举值时为了避免生成多余的代码和间接引用,可以使用常数枚举,常数枚举使用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
}
外部枚举和非外部枚举之间有一个重要的区别,正常的枚举中,没有初始化方法的成员被当作常数成员,对于非常数的外部枚举而言,没有初始化方法时被当作需要经过计算的。