TypeScript 是 JavaScript 的一个超集

TypeScript 是一种开源的编程语言,它添加了静态类型和其他一些特性,使得编写大型应用程序更加容易,支持 ECMAScript 6 标准。特点:

  1. 支持最新的 JavaScript 新特特性
  2. 支持代码静态检查
  3. 支持诸如 C、C++、Java、Go等后端语言中的特性(枚举、泛型、类型转换、命名空间、声明文件、类、接口等)

TypeScript 与 JavaScript 的区别:

  • JavaScript:是弱类型, 很多错误只有在运行时才会被发现
  • TypeScript:提供了一套静态检测机制, 可以帮助我们在编译时就发现错误

安装

首先,你需要安装 TypeScript 编译器。可以通过 Node.js 的包管理器 npm 来安装,打开命令行工具,运行以下命令安装 TypeScript:

1
npm install -g typescript

创建 TypeScript 文件:创建一个新的 TypeScript 文件,通常使用 .ts 后缀。你可以使用任何文本编辑器,如 Visual Studio Code,Sublime Text 或 Atom 来创建文件

在编写 TypeScript 代码后,你需要将 TypeScript 代码编译为 JavaScript 代码。在命令行中,进入 TypeScript 文件所在的目录,并运行以下命令:

1
tsc yourfile.ts

这将会生成一个与你的 TypeScript 文件同名的 JavaScript 文件

基础类型

为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用

布尔值

最基本的数据类型就是简单的 true / false 值,在 JavaScript 和 TypeScript 里叫做 boolean(其它语言中也一样)

1
let isDone: boolean = false;

数字

和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。这些浮点数的类型是 number。除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015 中引入的二进制和八进制字面量

1
2
3
4
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

字符串

JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。像其它语言里一样,我们使用 string 表示文本数据类型。和 JavaScript 一样,可以使用双引号(")或单引号(')表示字符串

1
2
let name: string = "bob";
name = "smith";

你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。这种字符串是被反引号包围(`),并且以${ expr }这种形式嵌入表达式

1
2
3
4
5
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 的方式效果相同:

1
2
let sentence: string = "Hello, my name is " + name + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";

数组

TypeScript 像 JavaScript 一样可以操作数组元素。有两种方式可以定义数组。第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

1
let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

1
let list: Array<number> = [1, 2, 3];

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。比如,你可以定义一对值分别为 stringnumber 类型的元组

1
2
3
4
5
6
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

1
2
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会使用联合类型替代:

1
2
3
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型

枚举

enum 类型是对 JavaScript 标准数据类型的一个补充。像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,从 0 开始为元素编号。你也可以手动的指定成员的数值。例如,我们将上面的例子改成从 1 开始编号:

1
2
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

1
2
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:

1
2
3
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 显示 'Green' 因为上面代码里它的值是 2

Any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。那么我们可以使用 any 类型来标记这些变量:

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对现有代码进行改写的时候,any 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。你可能认为 Object 有相似的作用,就像它在其它语言中那样。但是 Object 类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

1
2
3
4
5
6
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'

当你只知道一部分数据的类型时,any 类型也是有用的。比如,你有一个数组,它包含了不同的类型的数据:

1
2
let list: any[] = [1, true, "free"];
list[1] = 100;

Void

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void

1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefinednull

1
let unusable: void = undefined;

Null 和 Undefined

TypeScript 里,undefinednull 两者各自有自己的类型分别叫做 undefinednull。 和 void 相似,它们的本身的类型用处不是很大:

1
2
3
// 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

Never

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;变量也可能是 never 类型,当它们被永不为真的类型保护所约束时

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。即使 any 也不可以赋值给 never。下面是一些返回 never 类型的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 返回 never 的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

// 推断的返回值类型为 never
function fail() {
return error("Something failed");
}

// 返回 never 的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}

Object

object 表示非原始类型,也就是除 numberstringbooleansymbolnullundefined 之外的类型。使用 object 类型,就可以更好的表示像 Object.create 这样的API。例如:

1
2
3
4
5
6
7
8
9
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。通过类型断言这种方式可以告诉编译器,"相信我,我知道自己在干什么"。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。类型断言有两种形式:

其一是"尖括号"语法:

1
2
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

另一个为as语法:

1
2
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在 TypeScript 里使用JSX时,只有 as 语法断言是被允许的

编译配置

tsconfig.json 是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有 tsconfig.json,TypeScript 就认为这是项目的根目录。如果不指定配置文件的位置,tsc 就会在当前目录下搜索 tsconfig.json 文件,如果不存在,就到上一级目录搜索,直到找到为止。tsconfig.json 文件的格式,是一个 JSON 对象,最简单的情况可以只放置一个空对象{}。下面是一个示例:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5"
},
"include": ["./src/**/*"]
}

include

include 是一个数组,用于指定要包含在编译中的文件或文件夹的匹配模式。可以使用通配符来指定多个文件或文件夹

  • * 匹配零个或多个字符(不包括目录分隔符)
  • ? 匹配任意一个字符(不包括目录分隔符)
  • **/ 匹配嵌套到任何级别的任何目录
1
2
3
{
"include": ["src/**/*", "tests/**/*"]
}

exclude

exclude 是一个数组,用于指定要排除在编译之外的文件或文件夹的匹配模式。也可以使用通配符来排除多个文件或文件夹

1
2
3
4
{
"include": ["**/*"],
"exclude": ["**/*.spec.ts"]
}

extends

extends 属性用于继承其他配置文件的设置。通过指定一个继承的配置文件路径,可以继承该配置文件中的编译选项,并且可以在当前的 tsconfig.json 文件中进行进一步的自定义。 继承包括compilerOptions,files,include,exclude

1
2
3
{
"extends": "@tsconfig/node12/tsconfig.json"
}

references

references是一个数组,用于指定项目之间的引用关系。可以通过引用其他项目来实现项目之间的依赖管理

1
2
3
4
5
6
{
"references": [
{ "path": "../pkg1" },
{ "path": "../pkg2/tsconfig.json" }
]
}

files

files是一个数组,用于指定要编译的文件列表。如果指定了这个选项,编译器将只编译列出的文件,而不会自动包含其他文件

1
2
3
{
"files": ["a.ts", "b.ts"]
}

compilerOptions

compilerOptions 是一个对象,用于配置 TypeScript 编译器的选项。它可以设置诸如目标版本、模块系统、输出目录、严格类型检查等编译器相关的选项

target

target 选项告诉编译器你想将 TypeScript 编译成哪个版本的 ECMAScript。默认情况下,TypeScript 会将代码编译为最新的 ECMAScript 版本(目前是 ESNext),但你可以将 target 选项设置为 ES5、ES6 或者 ES7 等其他版本

1
2
3
4
5
{
"compilerOptions": {
"target": "ES5"
}
}

module

module 选项告诉编译器你想如何组织生成的 JavaScript 代码

1
2
3
4
5
{
"compilerOptions": {
"module": "commonjs"
}
}

在上面的示例中,我们将模块组织方式设置为 CommonJS。你还可以使用其他的模块组织方式,例如:AMD、UMD 和 ES6 等

lib

lib 选项告诉编译器你需要使用哪些类型库

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"lib": [
"es6",
"dom"
]
}
}

在上面的示例中,我们告诉 TypeScript 编译器我们需要使用 ES6 和 DOM 类型库。一般不用动

outDir

如果指定,.js(以及 .d.ts.js.map 等)文件将被发送到此目录。保留原始源文件的目录结构;如果未指定,.js 文件将在与生成它们的文件相同的目录中发出:.ts 文件:

1
2
3
4
5
{
"compilerOptions": {
"outDir": "dist"
}
}

使用这些设置运行 tsc 会将文件移动到指定的 dist 文件夹中:

1
2
3
4
5
example
├── dist
│ └── index.js
├── index.ts
└── tsconfig.json

outFile

将代码合并成一个文件,设置 outFile 后 所有的全局作用域中的代码会合并到同一个文件中。如果 module 是 systemamd,则所有模块文件也会在所有全局内容之后连接到此文件中

注意:outFile 不能使用,除非 module 为 None、System、或AMD。 此选项不能用于捆绑 CommonJS 或 ES6 模块

allowJs

允许在项目中导入 JavaScript 文件,而不仅仅是 .ts.tsx 文件。例如这个JS文件:

1
export const defaultCardDeck = "Heart";

当导入到 TypeScript 文件中时会引发错误:

1
2
3
import { defaultCardDeck } from "./card";

console.log(defaultCardDeck);

启用 allowJs 后导入正常。此标志可用于将 TypeScript 文件增量添加到 JS 项目中,方法是允许 .ts.tsx 文件与现有文件并存 JavaScript 文件。它还可以与 declarationemitDeclarationOnly 一起使用来创建 JS 文件的声明

checkJs

是否检查js文件语法。与allowJs协同工作。启用 checkJs 后,JavaScript 文件中会报告错误。例如,根据 TypeScript 附带的parseFloat 类型定义,这是不正确的 JavaScript:

1
2
// parseFloat only takes a string
module.exports.pi = parseFloat(3.142);

当导入到 TypeScript 模块中时不会收到任何错误。但是,如果您打开 checkJs,那么您将从 JavaScript 文件中收到错误消息

Argument of type 'number' is not assignable to parameter of type 'string'

removeComments

在转换为 JavaScript 时删除 TypeScript 文件中的所有注释。默认为 false。例如,这是一个带有 JSDoc 注释的 TypeScript 文件:

1
2
// The translation of 'Hello world' into Portuguese
export const helloWorldPTBR = "Olá Mundo";

当 removeComments 设置为 true 时:

1
export const helloWorldPTBR = "Olá Mundo";

未设置 removeComments 或将其设置为 false:

1
2
// The translation of 'Hello world' into Portuguese
export const helloWorldPTBR = "Olá Mundo";

noEmit

不生成编译后的文件

noEmitOnError

当有错误时不生成编译文件,默认为 false

alwaysStrict

确保文件在 ECMAScript 严格模式下进行解析,并为每个源文件发出"use strict",默认 false。ECMAScript strict 模式是在 ES5 中引入的,它为 JavaScript 引擎的运行时提供行为调整以提高性能,并抛出一组错误而不是默默地忽略它们

noImplicitAny

不允许使用隐式 any,开启 noImplicitAny,只要 TypeScript 推断出 any:,它就会发出错误:

1
2
3
function fn(s) {
console.log(s.subtr(3));
}

Parameter 's' implicitly has an 'any' type.

strictNullChecks

严格的检查空值。例如,使用此 TypeScript 代码,users.find 无法保证它确实会找到用户,但你可以编写代码就好像它会:

1
2
3
4
5
6
7
8
9
declare const loggedInUsername: string;

const users = [
{ name: "Oby", age: 12 },
{ name: "Heera", age: 32 },
];

const loggedInUser = users.find((u) => u.name === loggedInUsername);
console.log(loggedInUser.age);

将 strictNullChecks 设置为 true 将引发错误:

'loggedInUser' is possibly 'undefined'.

strict

所有严格模式的总开关,这个属性一定要写在最上面

传统的 JavaScript 程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是 ECMAScript 6 开始,JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本

1
2
3
4
5
6
7
8
9
10
11
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里,我们可以使用常用的面向对象模式。基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

修饰符

public

默认为 public,你也可以明确的将一个成员标记成 public。 我们可以用下面的方式来重写上面的 Animal 类:

1
2
3
4
5
6
7
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 时,它就不能在声明它的类的外部访问。比如:

1
2
3
4
5
6
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.

protected

protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected 成员在派生类中仍然可以访问。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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这意味着这个类不能在包含它的类外被实例化但是能被继承。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
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");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

readonly

你可以使用 readonly 关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化

1
2
3
4
5
6
7
8
9
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 是只读的.

构造函数

默认也是 public,如果我们在构造函数前面加一个 private那么这个类就不能在外部被实例化也不能被继承,这样的一种情况下我们只能够在类的内部添加一个静态方法,然后在这个静态方法当中创建这个类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person {
public name: string
private age: number
protected gender: boolean

constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}

sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
}
}

class Student extends Person {
private constructor (name: string, age: number) {
super(name, age)
}

static create (name: string, age: number) {
return new Student(name, age)
}
}

const jack = Student.create('jack', 18)

如果将构造函数标记为 protected,也是不能够在外部被实例化,但是相比于 private,它是运行继承的

getters / setters

TypeScript支持通过 getters / setters 来截取对对象成员的访问。它能帮助你有效的控制对对象成员的访问。它可以简写成 getset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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!");
}
}
}

静态属性

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。在这个例子里,我们使用 static 定义 origin,因为它是所有网格都会用到的属性。每个实例想要访问这个属性的时候,都要在 origin 前面加上类名。如同在实例属性上使用 this. 前缀来访问属性一样,这里我们使用 Grid. 来访问静态属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 scale
let grid2 = new Grid(5.0); // 5x scale

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类

抽象类做为其它派生类的基类使用。它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法

1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。抽象方法的语法与接口方法相似。两者都是定义方法签名但不包含方法体。然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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(); // 错误: 方法在声明的抽象类中不存在

函数

和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:

1
2
3
4
5
6
7
// Named function
function add(x, y) {
return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

函数类型

让我们为上面那个函数添加类型:

1
2
3
4
5
function add(x: number, y: number): number {
return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型,因此我们通常省略它

可选参数

JavaScript里,每个参数都是可选的,可传可不传。没传参的时候,它的值就是 undefined。 在 TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。比如,我们想让 last name 是可选的:

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}

let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

可选参数必须跟在必须参数后面。如果上例我们想让 first name 是可选的,那么就必须调整它们的位置,把 first name 放在后面

默认参数

在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。它们叫做有默认初始化值的参数。让我们修改上例,把 last name 的默认值设置为"Smith&quot:

1
2
3
4
5
6
7
8
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

与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined 值来获得默认值

剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。在 TypeScript 里,你可以把所有参数收集到一个变量里:

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

重载

方法是为同一个函数提供多个函数类型定义来进行函数重载。编译器会根据这个列表去处理函数的调用。下面我们来重载 pickCard 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 会产生错误

泛型

指的是我们去定义函数,接口,类的时候没有去定义具体类型,我们在使用的时候再去定义指定类型的这一种特征。以函数为例,泛型 就是我们在声明一个函数的时候不去指定一个类型,等我们调用的时候再传入一个具体的类型,这样做的目的是极大程度的复用我们的代码。比如 定义一个创建 number 类型和 string 类型数组的方法,把类型用一个泛型参数 T表示,把函数当中不明确类型用 T 去代表,在使用的时候再传递具体类型。如下:

1
2
3
4
5
6
7
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}

const numArr = createArray<number>(3, 100)
const strArr = createNumberArray<string>(3, 'foo')

其实 Array 是一个泛型类,在 typescript 中去定义这个 Array 类型时,它不知道我们使用它去存放什么样类型的数据,所以它就使用泛型参数,在我们去调用的时候再传入具体类型,这是一个泛型提现。总的来说,泛型就是在我们定义的时候把不明确的类型,变成一个参数,在我们使用的时候再传递这样的一个类型参数

接口

可以理解为一种规范或者契约,它可以用来约定对象的结构,我们去使用一个接口就要去遵循这个接口的所有约定。在 typescript 中,接口最直观的体现是,约定一个对象当中具体应该有那些成员,并且这些成员是什么类型的

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Post {
// 多个成员可使用 ','、';' 分割,也可以不用
title: string
content: string
}

function printPost (post: Post) {
console.log(post.title)
console.log(post.content)
}

printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
})

对于这个函数所接收的 post 对象,他就有一定的要求,所传入的对象必须要有一个 title 属性和 content 属性,只不过这种要求实际上是隐性的,它没有明确的表达出来,这种情况下我们就可以使用接口去表现出来这种约束

可选成员

如果说我们在一个对象当中,我们某个成员是可有可无的,那对于约束这个对象的接口来说,我们可以使用可选成员这样的一个特性

1
2
3
4
5
interface Post {
title: string
content: string
subtitle?: string // 可选成员
}

只读成员

初始化过后不能再修改

1
2
3
4
5
6
interface Post {
title: string
content: string
subtitle?: string // 可选成员
readonly summary: string // 只读成员
}

动态成员

一般用于一些有动态成员的对象,例如程序当中的缓存对象,它在运行当中会出现一些动态的键值

1
2
3
4
5
6
7
8
9
10
11
interface Cache {
// [属性名称(不是固定的,可以是任意名称), 键的类型]:值的类型
[prop: string]: string
}

// 创建一个 cache 对象实现这个 Cache 接口
const cache: Cache = {}

// 动态添加任意的成员,这些成员都必须遵循 Cache 接口的类型约束
cache.foo = 'value1'
cache.bar = 'value2'

类实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}

class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}

你也可以在接口中描述一个方法,在类里实现它,如同下面的 setTime 方法一样:

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

接口描述了类的公共部分,而不是公共和私有两部分。它不会帮你检查类是否具有某些私有成员

继承接口

和类一样,接口也可以相互继承。这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

接口继承类

当接口继承了一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。接口同样会继承到类的 privateprotected 成员。这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时这个接口类型只能被这个类或其子类所实现(implement)。

当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。这个子类除了继承至基类外与基类没有任何关系。 例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {

}