基础类型
介绍
ts包含的基础的数据单元:
- 数字(number)
- 字符串(string)
- 结构体
- 布尔值(boolean)
- 枚举(js中没有)
定义变量的方法
let [变量名]: [变量类型] = [变量值];
布尔值
let isDone: boolean = false;
数字
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。let decLiteral: number = 6;let hexLiteral: number = 0xf00d;let binaryLiteral: number = 0b1010;let octalLiteral: number = 0o744;
字符串
和js一样,用string表示文本数据类型。可以用双引号或者单引号表示字符串。
还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围(let name: string = "bob";name = "smith";
),并且以**`${ expr }`**这种形式嵌入表达式typescript let name: string =Gene; let age: number = 37; let sentence: string = `Hello, my name is ${name}.
I’ll be ${age+1} years old next month.`;
<a name="0QTNB"></a>### 数组TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 `[]`,表示由此类型元素组成的一个数组:```typescriptlet list: number[] = [1,2,3];
第二种方式是使用数组泛型,Array<元素类型>:
let list: Array<number> = [1,2,3];
元祖 Tuple
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。
//Declare a tuple typelet x: [string,number];//Initialize itx = ['hello',10];//ok// Initialize it incorrectlyx = [10, 'hello']; // Error
!注意初始化必须与声明的类型保持一致。
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OKconsole.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toStringx[6] = true; // Error, 布尔不是(string | number)类型
枚举(js中没有)
enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red,Green,Blue}let c: Color = Color.Green;
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:
enum Color{Red = 1,Green,Blue}let c: Color = Color.Green;
或者,全部都采用手动赋值:
enum Color{Red = 1,Green = 2,Blue = 4}let c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue}let colorName: string = Color[2];console.log(colorName); // 显示'Green'因为上面代码里它的值是2
Any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:
let notSure: any = 4;notSure = "maybe a string instead";notSure = false; // okay, definitely a boolean
在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4;notSure.ifItExists(); // okay, ifItExists might exist at runtimenotSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)let prettySure: Object = 4;prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, "free"];list[1] = 100;
Void
某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void {console.log("This is my warning message");}
声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:
let unusable: void = undefined;
Null和Undefined
TypeScript里,undefined和null两者各自有自己的类型分别叫做undefined和null。 和 void相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!let u: undefined = undefined;let n: null = null;
默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。
然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。
注意:我们鼓励尽可能地使用
--strictNullChecks,但在本手册里我们假设这个标记是关闭的。
Never
never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。
下面是一些返回never类型的函数:
// 返回never的函数必须存在无法达到的终点function error(message: string): never {throw new Error(message);}// 推断的返回值类型为neverfunction fail() {return error("Something failed");}// 返回never的函数必须存在无法达到的终点function infiniteLoop(): never {while (true) {}}
Object
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
使用object类型,就可以更好的表示像Object.create这样的API。例如:
declare function create(o: object | null): void;create({ prop: 0 }); // OKcreate(null); // OKcreate(42); // Errorcreate("string"); // Errorcreate(false); // Errorcreate(undefined); // Error
类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";let strLength: number = (<string>someValue).length;
另一个为as语法:
let someValue: any = "this is a string";let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。
let
通es6之后一样,尽可能用let来代替var。
变量声明
声明
let和const是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,它能阻止对一个变量再次赋值。
推荐使用let和const而避免使用var
let声明的写法和var的写法是完全一致的,但是其语义完全不同。
块作用域
当用let声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。
function f(input: boolean) {let a = 100;if (input) {// Still okay to reference 'a'let b = a + 1;return b;}// Error: 'b' doesn't exist herereturn b;}
这里我们定义了2个变量a和b。 a的作用域是f函数体内,而b的作用域是if语句块里。
在catch语句里声明的变量也具有同样的作用域规则。
try {throw "oh no!";}catch (e) {console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 它只是用来说明我们不能在 let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
a++; // illegal to use 'a' before it's declared;let a;
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
function foo() {// okay to capture 'a'return a;}// 不能在'a'被声明前调用'foo'// 运行时应该抛出错误foo();let a;
重定义及屏蔽
let不允许重复声明变量,否则会报错。
let x = 10;let x = 20; // 错误,不能在1个作用域里多次声明`x`
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
function f(x) {let x = 100; // error: interferes with parameter declaration}function g() {let x = 100;var x = 100; // error: can't have both declarations of 'x'}
并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。
function f(condition, x) {if (condition) {let x = 100;return x;}return x;}f(false, 0); // returns 0f(true, 0); // returns 100
尽量避免在块和其所包含的内部块中声明同一个名称的变量,以造成不必要的错误。
const声明
const 声明是声明变量的另一种方式。
const numLivesForCat = 9;
解构
解构数组
最简单的解构莫过于数组的解构赋值了:
let input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
这创建了2个命名变量 first 和 second。 相当于使用了索引,但更为方便:
first = input[0];second = input[1];
解构作用于已声明的变量会更好:
// swap variables[first, second] = [second, first];
作用于函数参数:
function f([first, second]: [number, number]) {console.log(first);console.log(second);}f(input);
你可以在数组里使用...语法创建剩余变量:
let [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:
let [first] = [1, 2, 3, 4];console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
对象解构
你也可以解构对象:
let o = {a: "foo",b: 12,c: "bar"};let { a, b } = o;
这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
({ a, b } = { a: "baz", b: 101 });
注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
你可以在对象里使用...语法创建剩余变量:
let { a, ...passthrough } = o;let total = passthrough.b + passthrough.c.length;
属性重命名
你也可以给属性以不同的名字:
let { a: newName1, b: newName2 } = o;
这里的语法开始变得混乱。 你可以将 a: newName1 读做 “a 作为 newName1“。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a;let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o;
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) {let { a, b = 1001 } = wholeObject;}
现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。
函数声明
解构也能用于函数声明。 看以下简单的情况:
type C = { a: string, b?: number }function f({ a, b }: C): void {// ...}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
function f({ a="", b=0 } = {}): void {// ...}f();
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
function f({ a, b = 0 } = { a: "" }): void {// ...}f({ a: "yes" }); // ok, default b = 0f(); // ok, default to {a: ""}, which then defaults b = 0f({}); // error, 'a' is required if you supply an argument
要小心使用解构。 从前面的例子可以看出,就算是最简单的解构表达式也是难以理解的。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。
展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:
let first = [1, 2];let second = [3, 4];let bothPlus = [0, ...first, ...second, 5];
这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了 first和second的一份浅拷贝。 它们不会被展开操作所改变。
你还可以展开对象:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { ...defaults, food: "rich" };
search的值为{ food: "rich", price: "$$", ambiance: "noisy" }。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { food: "rich", ...defaults };
那么,defaults里的food属性会重写food: "rich",在这里这并不是我们想要的结果。
对象展开还有其它一些意想不到的限制。 首先,它仅包含对象 自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:
class C {p = 12;m() {}}let c = new C();let clone = { ...c };clone.p; // okclone.m(); // error!
其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。
接口
个人理解ts主要的作用是将弱类型的js语言,变成强类型的语言。
TypeScript的核心原则之一是对值所具有的结构进行类型检查。
在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
接口初探
下面简单示例:
function printLabel(labelledObj: { label: string }) {console.log(labelledObj.label);}let myObj = { size: 10, label: "Size 10 Object" };printLabel(myObj);
类型检查器会查看printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。
下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string:
interface LabelledValue {label: string;}function printLabel(labelledObj: LabelledValue) {console.log(labelledObj.label);}let myObj = {size:10,label:"Size 10 Object"};printLabel(myObj);
LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“option bags”的例子:
interface SquareConfig {color?:string;width?:number;}function createSquare(config:SquareConfig):{color:string;area:number} {let newSquare = {color:"white",area:100};if(config.color){newSquare.color = config.color;}if(config.width){newSquare.area = config.width**2;}return newSquare;}let mySquare = createSquare({color:"black",width:5})console.log(mySquare.color,mySquare.area);
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare里的color属性名拼错,就会得到一个错误提示:
interface SquareConfig {color?: string;width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } {let newSquare = {color: "white", area: 100};if (config.clor) {// Error: Property 'clor' does not exist on type 'SquareConfig'newSquare.color = config.clor;}if (config.width) {newSquare.area = config.width * config.width;}return newSquare;}let mySquare = createSquare({color: "black"});
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:
interface Point {readonly x: number;readonly y: number;}
你可以通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了。
let p1: Point = { x: 10, y: 20 };p1.x = 5; // error!
TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];let ro: ReadonlyArray<number> = a;ro[0] = 12; // error!ro.push(5); // error!ro.length = 100; // error!a = ro; // error!
上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
let a: number[] = [1, 2, 3, 4];let ro: ReadonlyArray<number> = a;ro.forEach(t => {console.log(t);});let b:number[];//两种断言的写法不要忘了b = ro as number[];let c:number[] = <number[]>ro;
readonly vs const
最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly。
额外的属性检查
我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。
然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿 createSquare例子来说:
interface SquareConfig {color?: string;width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } {// ...}let mySquare = createSquare({ colour: "red", width: 100 });
注意传入createSquare的参数拼写为colour而不是color。 在JavaScript里,这会默默地失败。
你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。
然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
// error: 'colour' not expected in type 'SquareConfig'let mySquare = createSquare({ colour: "red", width: 100 });
绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig带有上面定义的类型的color和width属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig {color?: string;width?: number;[propName: string]: any;}
但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是color和width,那么就无所谓它们的类型是什么。
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 };let mySquare = createSquare(squareOptions);
要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入 color或colour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc {(source: string, subString: string): boolean;}
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
let mySearch: SearchFunc;mySearch = function(source: string, subString: string) {let result = source.search(subString);return result > -1;}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。
let mySearch: SearchFunc;mySearch = function(src, sub) {let result = src.search(sub);return result > -1;}
可索引的类型
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]或ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
interface StringArray {[index: number]: string;}let myArray: StringArray;myArray = ["Bob", "Fred"];let myStr: string = myArray[0];
上面例子里,我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 number去索引StringArray时会得到string类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。
class Animal {name: string;}class Dog extends Animal {breed: string;}// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!interface NotOkay {[x: number]: Animal;[x: string]: Dog;}
字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property和obj["property"]两种形式都可以。 下面的例子里, name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {[index: string]: number;length: number; // 可以,length是number类型name: string // 错误,`name`的类型与索引类型返回值的类型不匹配}
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {readonly [index: number]: string;}let myArray: ReadonlyStringArray = ["Alice", "Bob"];myArray[2] = "Mallory"; // error!
类类型
实现接口
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {currentTime: Date;}class Clock implements ClockInterface {currentTime: Date;constructor(h: number, m: number) { }}
你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:
interface ClockInterface {currentTime: Date;setTime(d: Date);}class Clock implements ClockInterface {currentTime: Date;setTime(d: Date) {this.currentTime = d;}constructor(h: number, m: number) { }}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
类静态部分与实例部分的区别
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor {new (hour: number, minute: number);}class Clock implements ClockConstructor {currentTime: Date;constructor(h: number, m: number) { }}
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。
interface ClockConstructor {new (hour: number, minute: number): ClockInterface;}interface ClockInterface {tick();}function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {return new ctor(hour, minute);}class DigitalClock implements ClockInterface {constructor(h: number, m: number) { }tick() {console.log("beep beep");}}class AnalogClock implements ClockInterface {constructor(h: number, m: number) { }tick() {console.log("tick tock");}}let digital = createClock(DigitalClock, 12, 17);let analog = createClock(AnalogClock, 7, 32);
因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。
继承接口
和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape {color: string;}interface Square extends Shape {sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape {color: string;}interface PenStroke {penWidth: number;}interface Square extends Shape, PenStroke {sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;square.penWidth = 5.0;
混合类型
先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {(start: number): string;interval: number;reset(): void;}function getCounter(): Counter {let counter = <Counter>function (start: number) { };counter.interval = 123;counter.reset = function () { };return counter;}let c = getCounter();c(10);c.reset();c.interval = 5.0;
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。
接口继承类
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。 这个子类除了继承至基类外与基类没有任何关系。 例:
class Control {private state: any;}interface SelectableControl extends Control {select(): void;}class Button extends Control implements SelectableControl {select() { }}class TextBox extends Control {select() { }}// 错误:“Image”类型缺少“state”属性。class Image implements SelectableControl {select() { }}class Location {}
在上面的例子里,SelectableControl包含了Control的所有成员,包括私有成员state。 因为 state是私有成员,所以只能够是Control的子类们才能实现SelectableControl接口。 因为只有 Control的子类才能够拥有一个声明于Control的私有成员state,这对私有成员的兼容性是必需的。
在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。 实际上, SelectableControl接口和拥有select方法的Control类是一样的。 Button和TextBox类是SelectableControl的子类(因为它们都继承自Control并有select方法),但Image和Location类并不是这样的。
类
类
下面看一个使用类的例子:
class Greeter {greeting: string;constructor(message: string) {this.greeting = message;}greet() {return "Hello, " + this.greeting;}}let greeter = new Greeter("world");
如果你使用过C#或Java,你会对这种语法非常熟悉。 我们声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法。
你会注意到,我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。
最后一行,我们使用 new构造了 Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个 Greeter类型的新对象,并执行构造函数初始化它。
继承
在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
class Animal {move(distanceInMeters: number = 0) {console.log(`Animal moved ${distanceInMeters}m.`);}}class Dog extends Animal {bark() {console.log('Woof! Woof!');}}const dog = new Dog();dog.bark();dog.move(10);dog.bark();
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里, Dog是一个 派生类,它派生自 Animal 基类,通过 extends关键字。 派生类通常被称作 子类,基类通常被称作 超类。
因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()和 move()。
下面我们来看个更加复杂的例子。
class Animal {name: string;constructor(theName: string) { this.name = theName; }move(distanceInMeters: number = 0) {console.log(`${this.name} moved ${distanceInMeters}m.`);}}class Snake extends Animal {constructor(name: string) { super(name); }move(distanceInMeters = 5) {console.log("Slithering...");super.move(distanceInMeters);}}class Horse extends Animal {constructor(name: string) { super(name); }move(distanceInMeters = 45) {console.log("Galloping...");super.move(distanceInMeters);}}let sam = new Snake("Sammy the Python");let tom: Animal = new Horse("Tommy the Palomino");sam.move();tom.move(34);
这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends关键字创建了 Animal的两个子类: Horse和 Snake。
与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。 Snake类和 Horse类都创建了 move方法,它们重写了从 Animal继承来的 move方法,使得 move方法根据不同的类而具有不同的功能。 注意,即使 tom被声明为 Animal类型,但因为它的值是 Horse,调用 tom.move(34)时,它会调用 Horse里重写的方法:
Slithering...Sammy the Python moved 5m.Galloping...Tommy the Palomino moved 34m.
公共,私有与受保护的修饰符
默认为 public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public来做修饰;例如,C#要求必须明确地使用 public指定成员是可见的。 在TypeScript里,成员都默认为 public。
你也可以明确的将一个成员标记成 public。 我们可以用下面的方式来重写上面的 Animal类:
class Animal {public name: string;public constructor(theName: string) { this.name = theName; }public move(distanceInMeters: number) {console.log(`${this.name} moved ${distanceInMeters}m.`);}}
理解 private
当成员被标记成 private时,它就不能在声明它的类的外部访问。比如:
class Animal {private name: string;constructor(theName: string) { this.name = theName; }}new Animal("Cat").name; // 错误: 'name' 是私有的.
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有 private或 protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。
下面来看一个例子,更好地说明了这一点:
class Animal {private name: string;constructor(theName: string) { this.name = theName; }}class Rhino extends Animal {constructor() { super("Rhino"); }}class Employee {private name: string;constructor(theName: string) { this.name = theName; }}let animal = new Animal("Goat");let rhino = new Rhino();let employee = new Employee("Bob");animal = rhino;animal = employee; // 错误: Animal 与 Employee 不兼容.
这个例子中有 Animal和 Rhino两个类, Rhino是 Animal类的子类。 还有一个 Employee类,其类型看上去与 Animal是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为 Animal和 Rhino共享了来自 Animal里的私有成员定义 private name: string,因此它们是兼容的。 然而 Employee却不是这样。当把 Employee赋值给 Animal的时候,得到一个错误,说它们的类型不兼容。 尽管 Employee里也有一个私有成员 name,但它明显不是 Animal里面定义的那个。
理解 protected
protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。例如:
class Person {protected name: string;constructor(name: string) { this.name = name; }}class Employee extends Person {private department: string;constructor(name: string, department: string) {super(name)this.department = department;}public getElevatorPitch() {return `Hello, my name is ${this.name} and I work in ${this.department}.`;}}let howard = new Employee("Howard", "Sales");console.log(howard.getElevatorPitch());console.log(howard.name); // 错误
注意,我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为 Employee是由 Person派生而来的。
构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,
class Person {protected name: string;protected constructor(theName: string) { this.name = theName; }}// Employee 能够继承 Personclass Employee extends Person {private department: string;constructor(name: string, department: string) {super(name);this.department = department;}public getElevatorPitch() {return `Hello, my name is ${this.name} and I work in ${this.department}.`;}}let howard = new Employee("Howard", "Sales");let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.
readonly修饰符
你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus {readonly name: string;readonly numberOfLegs: number = 8;constructor (theName: string) {this.name = theName;}}let dad = new Octopus("Man with the 8 strong legs");dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
参数属性
在上面的例子中,我们必须在Octopus类里定义一个只读成员 name和一个参数为 theName的构造函数,并且立刻将 theName的值赋给 name,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Octopus类的修改版,使用了参数属性:
class Octopus {readonly numberOfLegs: number = 8;constructor(readonly name: string) {}}
注意看我们是如何舍弃了 theName,仅在构造函数里使用 readonly name: string参数来创建和初始化 name成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。 使用 private限定一个参数属性会声明并初始化一个私有成员;对于 public和 protected来说也是一样。
存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用 get和 set。 首先,我们从一个没有使用存取器的例子开始。
class Employee {fullName: string;}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) {console.log(employee.fullName);}
我们可以随意的设置 fullName,这是非常方便的,但是这也可能会带来麻烦。
下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName的直接访问改成了可以检查密码的 set方法。 我们也加了一个 get方法,让上面的例子仍然可以工作。
let passcode = "secret passcode";class Employee {private _fullName: string;get fullName(): string {return this._fullName;}set fullName(newName: string) {if (passcode && passcode == "secret passcode") {this._fullName = newName;}else {console.log("Error: Unauthorized update of employee!");}}}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) {alert(employee.fullName);}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
静态属性
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static定义 origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性。
class Grid {static origin = {x: 0, y: 0};calculateDistanceFromOrigin(point: {x: number; y: number;}) {let xDist = (point.x - Grid.origin.x);let yDist = (point.y - Grid.origin.y);return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;}constructor (public scale: number) { }}let grid1 = new Grid(1.0); // 1x scalelet grid2 = new Grid(5.0); // 5x scaleconsole.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal {abstract makeSound(): void;move(): void {console.log('roaming the earch...');}}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。
abstract class Department {constructor(public name: string) {}printName(): void {console.log('Department name: ' + this.name);}abstract printMeeting(): void; // 必须在派生类中实现}class AccountingDepartment extends Department {constructor() {super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()}printMeeting(): void {console.log('The Accounting Department meets each Monday at 10am.');}generateReports(): void {console.log('Generating accounting reports...');}}let department: Department; // 允许创建一个对抽象类型的引用department = new Department(); // 错误: 不能创建一个抽象类的实例department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值department.printName();department.printMeeting();department.generateReports(); // 错误: 方法在声明的抽象类中不存在
高级技巧
构造函数
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的实例的类型。
class Greeter {greeting: string;constructor(message: string) {this.greeting = message;}greet() {return "Hello, " + this.greeting;}}let greeter: Greeter;greeter = new Greeter("world");console.log(greeter.greet());
这里,我们写了 let greeter: Greeter,意思是 Greeter类的实例的类型是 Greeter。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。
我们也创建了一个叫做 构造函数的值。 这个函数会在我们使用 new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:
let Greeter = (function () {function Greeter(message) {this.greeting = message;}Greeter.prototype.greet = function () {return "Hello, " + this.greeting;};return Greeter;})();let greeter;greeter = new Greeter("world");console.log(greeter.greet());
上面的代码里, let Greeter将被赋值为构造函数。 当我们调用 new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有 实例部分 与 静态部分 这两个部分。
让我们稍微改写一下这个例子,看看它们之间的区别:
class Greeter {static standardGreeting = "Hello, there";greeting: string;greet() {if (this.greeting) {return "Hello, " + this.greeting;}else {return Greeter.standardGreeting;}}}let greeter1: Greeter;greeter1 = new Greeter();console.log(greeter1.greet());let greeterMaker: typeof Greeter = Greeter;greeterMaker.standardGreeting = "Hey there!";let greeter2: Greeter = new greeterMaker();console.log(greeter2.greet());
这个例子里, greeter1与之前看到的一样。 我们实例化 Greeter类,并使用这个对象。 与我们之前看到的一样。
再之后,我们直接使用类。 我们创建了一个叫做 greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,”告诉我 Greeter标识符的类型”,也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker上使用 new,创建 Greeter的实例。
把类当做接口使用
如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point {x: number;y: number;}interface Point3d extends Point {z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
函数
介绍
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为 的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
函数
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种JavaScript中的函数:
// Named functionfunction add(x, y) {return x + y;}// Anonymous functionlet 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 typelet myAdd = function(x: number, y: number): number { return x + y; };// The parameters `x` and `y` have the type numberlet 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 parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // ah, just right
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?实现可选参数的功能。 比如,我们想让last name是可选的:
function buildName(firstName: string, lastName?: string) {if (lastName)return firstName + " " + lastName;elsereturn firstName;}let result1 = buildName("Bob"); // works correctly nowlet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet 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 parameterslet 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 parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet 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和箭头函数
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被设置成了window而不是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 herereturn () => {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 DeckcreateCardPicker: 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 runtimethis.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:指向它的调用者,如果没有调用者则默认指向window. 箭头函数的this: 指向箭头函数定义时所处的对象,而不是箭头函数使用时所在的对象,默认使用父级的this.
这是可行的因为箭头函数不会捕获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 cardif (typeof x == "object") {let pickedCard = Math.floor(Math.random() * x.length);return pickedCard;}// Otherwise just let them pick the cardelse 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 cardif (typeof x == "object") {let pickedCard = Math.floor(Math.random() * x.length);return pickedCard;}// Otherwise just let them pick the cardelse 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会产生错误。
函数重载的步骤简单总结为一下两步:
- 定义函数类型。
- 定义具体的函数。
泛型
介绍
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
例子如下:
function identity<T>(arg: T): T {return arg;}
我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
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 loggingIdentity<T>(arg: T[]): T[] {console.log(arg.length); // Array has a .length, so no more errorreturn arg;}
你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> {console.log(arg.length); // Array has a .length, so no more errorreturn arg;}
使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像 Array<T>一样。
泛型类型
泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T {return arg;}let myIdentity: <T>(arg: T) => T = identity;
我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
function identity<T>(arg: T): T {return arg;}let myIdentity: <U>(arg: U) => U = identity;
我们还可以使用带有调用签名的对象字面量来定义泛型函数:
function identity<T>(arg: T): T {return arg;}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; };console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
泛型约束
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。
function loggingIdentity<T>(arg: T): T {console.log(arg.length); // Error: T doesn't have .lengthreturn arg;}
相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .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 errorreturn arg;}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
在泛型约束中使用类型参数
你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。
function getProperty(obj: T, key: K) {return obj[key];}let x = { a: 1, b: 2, c: 3, d: 4 };getProperty(x, "a"); // okaygetProperty(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 Direction {Up = 1,Down,Left,Right}
如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。
我们还可以完全不使用初始化器:
enum Direction {Up,Down,Left,Right,}
现在, Up的值为 0, Down的值为 1等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。
使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:
enum Response {No = 0,Yes = 1,}function respond(recipient: string, message: Response): void {// ...}respond("Princess Caroline", Response.Yes)
数字枚举可以被混入到 计算过的和常量成员(如下所示)。 简短地说,不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。 换句话说,下面的情况是不被允许的:
enum E {A = getSomeValue(),B, // error! 'A' is not constant-initialized, so 'B' needs an initializer}
字符串枚举
字符串枚举的概念很简单,但是有细微的 运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Direction {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}
由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。 换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的 - 它并不能表达有用的信息(尽管 反向映射会有所帮助),字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。
异构枚举(Heterogeneous enums)
从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:
enum BooleanLikeHeterogeneousEnum {No = 0,Yes = "YES",}
计算的和常量成员
每个枚举成员都带有一个值,它可以是 常量或 计算出来的。 当满足如下条件时,枚举成员被当作是常量:
它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值
0:// E.X is constant:enum E { X }
它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。 ```typescript // All enum members in ‘E1’ and ‘E2’ are constant.
enum E1 { X, Y, Z }
enum E2 { A = 1, B, C }
- 枚举成员使用 _常量枚举表达式 _初始化。 常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:1. 一个枚举表达式字面量(主要是字符串字面量或数字字面量)1. 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)1. 带括号的常量枚举表达式1. 一元运算符 `+`, `-`, `~`其中之一应用在了常量枚举表达式1. 常量枚举表达式做为二元运算符 `+`, `-`, `*`, `/`, `%`, `<<`, `>>`, `>>>`, `&`, `|`, `^`的操作对象。 若常数枚举表达式求值后为 `NaN`或 `Infinity`,则会在编译阶段报错。所有其它情况的枚举成员被当作是需要计算得出的值。```typescriptenum FileAccess {// constant membersNone,Read = 1 << 1,Write = 1 << 2,ReadWrite = Read | Write,// computed memberG = "123".length}
联合枚举与枚举成员的类型
存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为
- 任何字符串字面量(例如:
"foo","bar","baz") - 任何数字字面量(例如:
1,100) - 应用了一元
-符号的数字字面量(例如:-1,-100)
当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。
首先,枚举成员成为了类型! 例如,我们可以说某些成员 只能是枚举成员的值:
enum ShapeKind {Circle,Square,}interface Circle {kind: ShapeKind.Circle;radius: number;}interface Square {kind: ShapeKind.Square;sideLength: number;}let c: Circle = {kind: ShapeKind.Square,// ~~~~~~~~~~~~~~~~ Error!radius: 100,}
另一个变化是枚举类型本身变成了每个枚举成员的 联合。 虽然我们还没有讨论联合类型,但你只要知道通过联合枚举,类型系统能够利用这样一个事实,它可以知道枚举里的值的集合。 因此,TypeScript能够捕获在比较值的时候犯的愚蠢的错误。 例如:
enum E {Foo,Bar,}function f(x: E) {if (x !== E.Foo || x !== E.Bar) {// ~~~~~~~~~~~// Error! Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'.}}
这个例子里,我们先检查 x是否不是 E.Foo。 如果通过了这个检查,然后 ||会发生短路效果, if语句体里的内容会被执行。 然而,这个检查没有通过,那么 x则 只能为 E.Foo,因此没理由再去检查它是否为 E.Bar。
运行时的枚举
枚举是在运行时真正存在的对象。 例如下面的枚举:
enum E {X, Y, Z}
的的确确能够被函数传递:
function f(obj: { X: number }) {return obj.X;}// Works, since 'E' has a property named 'X' which is a number.f(E);
反向映射
除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:
enum Enum {A}let a = Enum.A;let nameOfA = Enum[a]; // "A"
TypeScript可能会将这段代码编译为下面的JavaScript:
var Enum;(function (Enum) {Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));var a = Enum.A;var nameOfA = Enum[a]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
要注意的是 不会为字符串枚举成员生成反向映射。
const 枚举
大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 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}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
类型推断
基础
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子
let x = 3;
变量x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。
大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。
最佳通用类型
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,
let x = [0, 1, null];
为了推断x的类型,我们必须考虑所有元素的类型。 这里有两种选择: number和null。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
这里,我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]。
上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
window.onmousedown = function(mouseEvent) {console.log(mouseEvent.button); //<- Error};
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent参数的类型了。 如果函数表达式不是在上下文类型的位置, mouseEvent参数的类型需要指定为any,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
window.onmousedown = function(mouseEvent: any) {console.log(mouseEvent.button); //<- Now, no error is given};
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
function createZoo(): Animal[] {return [new Rhino(), new Elephant(), new Snake()];}
这个例子里,最佳通用类型有4个候选者:Animal,Rhino,Elephant和Snake。 当然, Animal会被做为最佳通用类型。
类型兼容性
介绍
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:
interface Named {name: string;}class Person {name: string;}let p: Named;// OK, because of structural typingp = new Person();
在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。
TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。
关于可靠性的注意事项
TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。
开始
TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:
interface Named {name: string;}let x: Named;// y's inferred type is { name: string; location: string; }let y = { name: 'Alice', location: 'Seattle' };x = y;
这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是name的string类型成员。y满足条件,因此赋值正确。
检查函数参数时使用相同的规则:
function greet(n: Named) {console.log('Hello, ' + n.name);}greet(y); // OK
注意,y有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容。
这个比较过程是递归进行的,检查每个成员及子成员。
比较两个函数
相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:
let x = (a: number) => 0;let y = (b: number, s: string) => 0;y = x; // OKx = 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 argumentsitems.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; // OKy = x; // Error, because x() lacks a location property
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多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 commonlistenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));// Undesirable alternatives in presence of soundnesslistenEvent(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 typeslistenEvent(EventType.Mouse, (e: number) => console.log(e));
可选参数及剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded。
有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:
function invokeLater(args: any[], callback: (...args: any[]) => void) {/* ... Invoke callback with 'args' ... */}// Unsound - invokeLater "might" provide any number of argumentsinvokeLater([1, 2], (x, y) => console.log(x + ', ' + y));// Confusing (x and y are actually required) and undiscoverableinvokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
enum Status { Ready, Waiting };enum Color { Red, Blue, Green };let status = Status.Ready;status = Color.Green; // Error
类
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal {feet: number;constructor(name: string, numFeet: number) { }}class Size {feet: number;constructor(numFeet: number) { }}let a: Animal;let s: Size;a = s; // OKs = a; // OK
类的私有成员和受保护成员
类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // OK, because y matches structure of x
上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty<T> {data: T;}let x: NotEmpty<number>;let y: NotEmpty<string>;x = y; // Error, because x and y are not compatible
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
let identity = function<T>(x: T): T {// ...}let reverse = function<U>(y: U): U {// ...}identity = reverse; // OK, because (x: any) => any matches (y: any) => any
高级主题
子类型与赋值
目前为止,我们使用了“兼容性”,它在语言规范里没有定义。 在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。
高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:
function extend<T, U>(first: T, second: U): T & U {let result = <T & U>{};for (let id in first) {(<any>result)[id] = (<any>first)[id];}for (let id in second) {if (!result.hasOwnProperty(id)) {(<any>result)[id] = (<any>second)[id];}}return result;}class Person {constructor(public name: string) { }}interface Loggable {log(): void;}class ConsoleLogger implements Loggable {log() {// ...}}var jim = extend(new Person("Jim"), new ConsoleLogger());var n = jim.name;jim.log();
联合类型(Union Types)
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number或 string类型的参数。 例如下面的函数:
/*** Takes a string and adds "padding" to the left.* If 'padding' is a string, then 'padding' is appended to the left side.* If 'padding' is a number, then that number of spaces is added to the left side.*/function padLeft(value: string, padding: any) {if (typeof padding === "number") {return Array(padding + 1).join(" ") + value;}if (typeof padding === "string") {return padding + value;}throw new Error(`Expected string or number, got '${padding}'.`);}padLeft("Hello world", 4); // returns " Hello world"
padLeft存在一个问题, padding参数的类型指定成了 any。 这就是说我们可以传入一个既不是 number也不是 string类型的参数,但是TypeScript却不报错。
let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
代替 any, 我们可以使用 联合类型做为 padding的参数:
/*** Takes a string and adds "padding" to the left.* If 'padding' is a string, then 'padding' is appended to the left side.* If 'padding' is a number, then that number of spaces is added to the left side.*/function padLeft(value: string, padding: string | number) {// ...}let indentedString = padLeft("Hello world", true); // errors during compilation
联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {fly();layEggs();}interface Fish {swim();layEggs();}function getSmallPet(): Fish | Bird {// ...}let pet = getSmallPet();pet.layEggs(); // okaypet.swim(); // errors
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B,我们能够 确定的是它包含了 A 和 B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 pet.fly()就出错了。
类型保护与区分类型(Type Guards and Differentiating Types)
联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为 Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。
let pet = getSmallPet();// 每一个成员访问都会报错if (pet.swim) {pet.swim();}else if (pet.fly) {pet.fly();}
为了让这段代码工作,我们要使用类型断言:
let pet = getSmallPet();if ((<Fish>pet).swim) {(<Fish>pet).swim();}else {(<Bird>pet).fly();}
用户自定义的类型保护
这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet的类型的话就好了。
TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {return (<Fish>pet).swim !== undefined;}
在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了if (isFish(pet)) {pet.swim();}else {pet.fly();}
注意TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型。
typeof类型保护
现在我们回过头来看看怎么使用联合类型书写 padLeft代码。 我们可以像下面这样利用类型断言来写:
function isNumber(x: any): x is number {return typeof x === "number";}function isString(x: any): x is string {return typeof x === "string";}function padLeft(value: string, padding: string | number) {if (isNumber(padding)) {return Array(padding + 1).join(" ") + value;}if (isString(padding)) {return padding + value;}throw new Error(`Expected string or number, got '${padding}'.`);}
然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
function padLeft(value: string, padding: string | number) {if (typeof padding === "number") {return Array(padding + 1).join(" ") + value;}if (typeof padding === "string") {return padding + value;}throw new Error(`Expected string or number, got '${padding}'.`);}
这些 typeof类型保护只有两种形式能被识别: typeof v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof类型保护
如果你已经阅读了 typeof类型保护并且对JavaScript里的 instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。instanceof类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:
interface Padder {getPaddingString(): string}class SpaceRepeatingPadder implements Padder {constructor(private numSpaces: number) { }getPaddingString() {return Array(this.numSpaces + 1).join(" ");}}class StringPadder implements Padder {constructor(private value: string) { }getPaddingString() {return this.value;}}function getRandomPadder() {return Math.random() < 0.5 ?new SpaceRepeatingPadder(4) :new StringPadder(" ");}// 类型为SpaceRepeatingPadder | StringPadderlet padder: Padder = getRandomPadder();if (padder instanceof SpaceRepeatingPadder) {padder; // 类型细化为'SpaceRepeatingPadder'}if (padder instanceof StringPadder) {padder; // 类型细化为'StringPadder'}
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的
prototype属性的类型,如果它的类型不为any的话 - 构造签名所返回的类型的联合
可以为null的类型
TypeScript具有两种特殊的类型, null和 undefined,它们分别具有值null和undefined. 我们在基础类型一节里已经做过简要说明。 默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型。 null与 undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为 价值亿万美金的错误。
--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null或 undefined。 你可以使用联合类型明确的包含它们:
let s = "foo";s = null; // 错误, 'null'不能赋值给'string'let sn: string | null = "bar";sn = null; // 可以sn = undefined; // error, 'undefined'不能赋值给'string | null'
注意,按照JavaScript的语义,TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。
可选参数和可选属性
使用了 --strictNullChecks,可选参数会被自动地加上 | undefined:
function f(x: number, y?: number) {return x + (y || 0);}f(1, 2);f(1);f(1, undefined);f(1, null); // error, 'null' is not assignable to 'number | undefined'
可选属性也会有同样的处理:
class C {a: number;b?: number;}let c = new C();c.a = 12;c.a = undefined; // error, 'undefined' is not assignable to 'number'c.b = 13;c.b = undefined; // okc.b = null; // error, 'null' is not assignable to 'number | undefined'
类型保护和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:
function f(sn: string | null): string {if (sn == null) {return "default";}else {return sn;}}
这里很明显地去除了 null,你也可以使用短路运算符:
function f(sn: string | null): string {return sn || "default";}
如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:
function broken(name: string | null): string {function postfix(epithet: string) {return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null}name = name || "Bob";return postfix("great");}function fixed(name: string | null): string {function postfix(epithet: string) {return name!.charAt(0) + '. the ' + epithet; // ok}name = name || "Bob";return postfix("great");}
本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name {if (typeof n === 'string') {return n;}else {return n();}}
起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = {value: T;left: Tree<T>;right: Tree<T>;}
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList<T> = T & { next: LinkedList<T> };interface Person {name: string;}var people: LinkedList<Person>;var s = people.name;var s = people.next.name;var s = people.next.next.name;var s = people.next.next.next.name;
然而,类型别名不能出现在声明右侧的任何地方。
type Yikes = Array<Yikes>; // error
接口vs类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface,但悬停在 aliased上时,显示的却是对象字面量类型。
type Alias = { num: number }interface Interface {num: number;}declare function aliased(arg: Alias): Alias;declare function interfaced(arg: Interface): Interface;
另一个重要区别是类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";class UIElement {animate(dx: number, dy: number, easing: Easing) {if (easing === "ease-in") {// ...}else if (easing === "ease-out") {}else if (easing === "ease-in-out") {}else {// error! should not pass null or undefined.}}}let button = new UIElement();button.animate(0, 0, "ease-in");button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
