TypeScript
1. TypeScript 简介
TypeScript 是由微软开发的编程语言。TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改。
1.1. TypeScript 安装
可以使用 npm 来安装 TypeScript 环境:
$ npm install -g typescript
安装成功后,可以使用 tsc
来编译 TypeScript 为 JavaScript。
如有 TypeScript 源文件 test.ts ,内容如下:
var message : string = "Hello World" console.log(message)
执行以下命令可以将 TypeScript 转换为 JavaScript 代码:
$ tsc test.ts
运行上面命令后,与 test.ts 的同一目录中会生成一个 test.js 文件,代码如下:
var message = "Hello World"; console.log(message);
这就是编译后得到的 JavaScript 文件。
2. 基本类型
TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。TypeScript 中推荐使用 let
和 const
来声明变量和常量,不推荐使用 var
(“函数作用域”,有一些怪异的行为)声明变量。
2.1. 布尔值
最基本的数据类型就是简单的 true/false 值,在 JavaScript 和 TypeScript 里叫做 boolean。
let isDone: boolean = false;
2.2. 数字
和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。
let decLiteral: number = 6; let hexLiteral: number = 0xf00d; let binaryLiteral: number = 0b1010; let octalLiteral: number = 0o744;
2.3. 字符串
和 JavaScript 一样,可以使用双引号或单引号表示字符串。
let name1: string = "bob"; let name2: string = 'bob';
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。这种字符串是被反引号包围(`),并且以 ${ expr }
这种形式嵌入表达式。如:
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.`;
上句中定义的 sentence 与下面效果相同:
let sentence: string = "Hello, my name is " + name + ".\n\n" + "I'll be " + (age + 1) + " years old next month.";
2.4. 数组
两种方式可以定义数组。第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3]; // 定义数组方式一
第二种方式是使用数组泛型,Array<元素类型>:
let list: Array<number> = [1, 2, 3]; // 定义数组方式二
2.5. 元组
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string 和 number 类型的元组。
// Declare a tuple type let x: [string, number]; // Initialize it x = ['hello', 10]; // OK // Initialize it incorrectly x = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型 x[6] = true; // Error, 布尔不是(string | number)类型
联合类型是高级主题,我们会在后面讨论它。
2.6. 枚举(enum)
enum 类型是对 JavaScript 标准数据类型的一个补充。
enum Color {Red, Green, Blue} let c: Color = Color.Green;
默认情况下,从 0 开始为元素编号。你也可以手动的指定成员的数值。例如,我们将上面的例子改成从 1 开始编号:
enum Color {Red = 1, Green, Blue} let c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue} let colorName: string = Color[2]; alert(colorName); // 显示'Green'
2.7. 任意值(any)
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
let notSure: any = 4; notSure = "maybe a string instead"; notSure = false; // okay, definitely a boolean
你可能认为 Object
有相似的作用,就像它在其它语言中那样。 但是 Object 类型的变量只是允许你给它赋任意值——但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4; notSure.ifItExists(); // okay, ifItExists might exist at runtime notSure.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'.
2.8. 空值(void)
某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:
function warnUser(): void { alert("This is my warning message"); }
声明一个 void
类型的变量没有什么大用,因为你只能为它赋予 undefined
和 null
:
let unusable: void = undefined;
2.9. 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 和它们各自。
2.10. 永不存在的值的类型(never)
never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;变量也可能是 never 类型,当它们被永不为真的类型保护所约束时。
下面是一些返回 never 类型的函数:
// 返回never的函数必须存在无法达到的终点 function error(message: string): never { throw new Error(message); } // 返回never的函数必须存在无法达到的终点 function infiniteLoop(): never { while (true) { } }
2.11. 类型断言
类型断言有两种形式。其一是“尖括号”语法:
let someValue: any = "this is a string"; let strLength: number = (<string>someValue).length; // 断言 someValue 为 string 类型
另一个为 as
语法:
let someValue: any = "this is a string"; let strLength: number = (someValue as string).length; // 断言 someValue 为 string 类型
两种形式是等价的。至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 里使用 JSX 时,只支持 as 语法(上面第二种形式)的类型断言。
3. 泛型
在像 C#和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。
3.1. 泛型之 Hello World
假设有个 identity 函数,这个函数会返回任何传入它的值。我们可以这样定义它:
function identity(arg: any): any { return arg; }
使用 any
类型会导致这个函数可以接收任何类型的 arg 参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
使用泛型函数可以解决这个问题,
function identity<T>(arg: T): T { // 泛型函数 return arg; }
不同于使用 any,它不会丢失信息,它可以实现更严格的约束:传入什么类型就返回相同的类型。
我们定义了泛型函数后,可以用两种方法使用。第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定 T
的类型:
let output = identity("myString"); // type of output will be 'string'
注意我们没必要使用尖括号 <>
来明确地传入类型;编译器可以查看 myString 的值,然后把 T
设置为它的类型。类型推论帮助我们保持代码精简和高可读性。
3.2. 泛型类
泛型类使用 <>
括起泛型类型,跟在类名后面。
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; };
上面例子中,使用泛型类时指定的是 number
,也可以使用字符串或其它更复杂的类型。如:
let stringNumeric = new GenericNumber<string>(); stringNumeric.zeroValue = ""; stringNumeric.add = function(x, y) { return x + y; }; alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
4. 高级类型
4.1. 交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。交叉类型的语法是使用 &
连接多个类型,例如, Person & Serializable & Loggable
同时是 Person 和 Serializable 和 Loggable。就是说这个类型的对象同时拥有了这三种类型的成员。
下面是交叉类型的例子:
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();
4.2. 联合类型(Union Types)
联合类型表示一个值可以是几种类型之一。 我们用竖线 |
分隔每个类型,比如 number | string | boolean
表示一个值可以是 number
, string
, 或 boolean
。
/** * 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) { // padding是联合类型 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"
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors
4.3. 类型别名(Type Aliases)
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string; // Name 是 string 的类型别名 type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } }
起别名不会新建一个类型——它创建了一个新名字来引用那个类型。给原始类型起别名通常没什么用,尽管可以作为文档的一种形式使用。
4.3.1. 接口 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 其它类型)。 因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
5. 接口
TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。在 TypeScript 里, 接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
interface IPerson { // 这个接口定义了2个属性和1个函数 firstName: string, lastName: string, sayHi: ()=>string } var customer : IPerson = { firstName: "Tom", // 如果指定的不是字符串类型,TypeScript会报错 lastName: "Hanks", sayHi: ():string =>{return "Hi there"} } console.log(customer.firstName) console.log(customer.lastName) console.log(customer.sayHi())
5.1. 可选属性
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ?
符号。
interface SquareConfig { color?: string; // color为可选属性 width?: number; // width为可选属性 } 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 * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare 里的 color 属性名拼错,就会得到一个错误提示:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.color) { // 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"});
5.2. 只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 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!
5.3. 接口描述函数类型
接口能够描述 JavaScript 中对象拥有的各种各样的外形。除了描述带有属性的普通对象外,接口也可以描述函数类型。
比如:
interface SearchFunc { (source: string, subString: string): boolean; }
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
let mySearch: SearchFunc; mySearch = function(source: string, subString: string): boolean { let result = source.search(subString); return result > -1; }
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。比如下面例子也是正确的:
let mySearch: SearchFunc; mySearch = function(src: string, sub: string): boolean { // 和接口中的参数名source, subString不相同没关系 let result = src.search(sub); return result > -1; }
5.4. 接口和数组
使用接口可以对数组进行一定的约束(索引值和元素设置为不同类型)。如:
interface namelist { [index:number]:string // 这个接口表示数组的索引必需是number,而元素类型必需是string } var list2:namelist = ["John",1,"Bran"] // 错误。元素 1 不是 string 类型 interface ages { [index:string]:number // 这个接口表示数组的下标必需是string,而元素类型必需是number } var agelist:ages; agelist["John"] = 15 // 正确 agelist[2] = "nine" // 错误,索引不是string,元素不是number
5.5. 接口继承
接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
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;