基础

类型推论

如果没有明确的指定类型, 那么 TypeScript 会依照类型推论(Type Inference) 的规则推断出一个类型

TypeScript 会在没有明确的指定类型的时候推测出一个类型, 这就是类型推论; 如果定义的时候没有赋值, 不管之后有没有赋值, 都会被推断成 any 类型而完全不被类型检查(见 any⬆)

1
2
3
// 以下代码虽然没有指定类型, 但是会在编译时报错
let num = 'seven'; // => 等价于 let num: string = 'seven'
num = 7 // error TS2322: Type 'number' is not assignable to type 'string'.

type 类型别名

类型别名用来给一个类型起新名字, 常用于联合类型

1
2
3
4
5
6
7
8
9
10
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}

联合类型

联合类型(Union Types) 表示取值可以为多种类型中的一种

使用: | 分割每个类型

1
2
3
4
let example: string | number;
example = 'seven';
example = 7;
example = true; // error TS2322

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候, 只能访问此联合类型的所有类型里共有的属性或方法

1
2
3
4
5
6
7
8
9
10
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
// length 不是 string 和 number 的共有属性, 所以会报错
// 访问 string 和 number 的共有属性是没问题的
function getString(somthing: string | number): number {
return something.toString();
}

联合类型的变量在被赋值的时候, 会根据类型推论的规则推断出一个类型

1
2
3
4
5
let example: string | number;
example = 'seven';
example.length; // 5
exmaple = 7;
example.length; // error TS2339: Property 'length' does not exist on type 'number'.

声明文件

  • declare var 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// <reference /> 三斜线指令

Tuple 元组

数组合并了相同类型的对象, 元组合并了不同类型的对象

元组类型允许表达固定数量的已知类型集合, 但这些类型不必是相同的(用于表示多类型的数组), 如果访问未定义类型的项, 会自动使用前面类型的 union type

元组起源于函数编程语言, 如 F#

1
2
3
4
5
6
7
8
9
10
// 声明一个元组类型
// 注: 当直接对元组类型的变量进行初始化或者赋值的时候, 需要提供所有元组类型中指定的项
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error
// 可以使用数字检索一个已知的元素, 但需要注意类型正确
x[0].substr(1); // OK
x[1].substr(1); // Error, 'number' does not have 'substring'
// 可以赋值其中一项
x[0] = 'bye'

当访问的索引超过边界(越界)时, 会显示以下错误, 就要使用联合类型(类型被限制为元组中每个类型的联合类型)处理

1
2
x[3] = 'world' // Error, Property '3' does not exist on type '[string, number]'.
x[5].toString() // Error, Property '5' does not exist on type '[string, number]'.

Enum 枚举

TypeScript 扩展了 JavaScript 原生的标准数据类型集, 增加了枚举类型(enum); 枚举是一种很有用的数据类型, 就像 C# 等语言中一样, 枚举是一种为数字值集赋予更友好名称的方法, 用于定义一组数字值/取值被限定在一定范围内的场景

1
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚举成员默认从 0 开始赋值递增的数字, 同时也会对枚举值到枚举名进行反向映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
Days['Sun'] === 0 // true
Days['Mon'] === 1 // true
Days[0] === 'Sun' // true
Days[1] === 'Mon' // true
// 实际上上述会被编译为
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

手动赋值

可以通过手动设置其成员之一的值来改变它, 也可以给所有的枚举元素设置数值, 未手动赋值的项会接着上一个枚举项递增

枚举类型有一个便捷特性, 可以直接用数值来查找其对应的枚举属性的名称

1
2
3
4
5
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
Days["Sun"] === 7; // true
Days["Mon"] === 1; // true
Days["Tue"] === 2; // true
Days["Sat"] === 6; // true

如果未手动赋值的枚举项与手动赋值的重复了, TypeScript 是不会察觉到这一点的, 所以使用的时候需要注意, 最好不要出现覆盖的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
Days["Sun"] === 3; // true
Days["Wed"] === 3; // true
Days[3] === "Sun"; // false
Days[3] === "Wed"; // true
// 以下是编译结果
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));

手动赋值的枚举项可以不是数字, 但是需要使用类型断言来让 tsc 无视类型检查(编译来的 js 仍是可用的)

1
2
3
4
5
6
7
8
9
10
11
12
enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>'S'};
// 编译结果
var Days;
(function (Days) {
Days[Days["Sun"] = 7] = "Sun";
Days[Days["Mon"] = 8] = "Mon";
Days[Days["Tue"] = 9] = "Tue";
Days[Days["Wed"] = 10] = "Wed";
Days[Days["Thu"] = 11] = "Thu";
Days[Days["Fri"] = 12] = "Fri";
Days[Days["Sat"] = "S"] = "Sat";
})(Days || (Days = {}));

手动赋值的枚举项也可以是小数或者是负数, 此时后续未手动赋值的项的递增步长仍为 1

1
2
3
4
enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};
Days['Sun'] === 7; // true
Days['Mon'] === 1.5; // true
Days['Sat'] === 6.5; // true

常数项和计算所得项

枚举项有两种类型: 常数项(constant member) 和计算所得项(computed member)

注意: 如果紧接在计算所得项后面的是未手动赋值的项, 那么就会因为无法获取初始值而报错

1
2
3
4
5
// 常数项
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
// 计算所得项 Green = 'green'.length
enum Color {Red, Blue, Green = 'green'.length}; // OK
enum Color {Red = 'red'.length, Blue, Green}; // error TS1061: Enum member must have initializer

当满足以下条件时, 枚举成员被当作是常数

  • 不具有初始化函数并且之前的枚举成员是常数; 在这种情况下, 当前枚举成员的值为上一个枚举成员的值加 1; 但第一个枚举元素是个例外, 如果它没有初始化方法, 那么它的初始值为 0
  • 枚举成员使用常数枚举表达式初始化; 常数枚举表达式是 TypeScript 表达式的子集, 它可以在编译阶段求值; 当一个表达式满足下面条件之一时, 它就是一个常数枚举表达式:
    • 数字字面量
    • 引用之前定义的常数枚举成员(可以是在不同的枚举类型中定义的)如果这个成员是在同一个枚举类型中定义的, 可以使用非限定名来引用
      带括号的常数枚举表达式
    • +, -, ~ 一元运算符应用于常数枚举表达式
    • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元运算符, 常数枚举表达式做为其一个操作对象; 若常数枚举表达式求值后为 NaN 或 Infinity, 则会在编译阶段报错

所有其它情况的枚举成员被当作是需要计算得出的值

常数枚举

使用 const enum 定义的枚举类型, 和普通枚举的区别是, 它会在编译阶段被删除, 并且不能包含计算成员, 如果包含了计算成员则会在编译阶段报错

1
2
3
4
5
6
7
8
9
10
11
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 */]
// 如果包含了计算成员
const enum Color {Red, Blue, Green = 'green'.length}; // error TS2474: In 'const' enum declarations member initializer must be constant expression

外部枚举

使用 declare enum 定义的枚举类型

declare 定义的类型只会用于编译时的检查, 编译结果中会被删除

1
2
3
4
5
6
7
8
9
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
// 编译结果
var directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

外部枚举和声明语句一样, 常出现在声明文件中; 同时使用 declare 和 const 也是可以的

1
2
3
4
5
6
7
8
9
declare 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 */]

Never

给永远不会结束的函数使用

never 类型表示从未出现的值的类型; 例如: never 是一个函数表达式或一个箭头函数表达式的返回类型, 它总是抛出一个异常或一个从不返回的异常, 当被任何类型保护(不可能为真)限制时, 变量也会获得类型 never

never 类型是每个类型的子类型, 并且可以赋值给每个类型; 然而, 任何类型都不是 never 的子类型或可分配给 never 的子类型(除了never 本身之外); 即使是 any 也不能分配给 never

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数返回 never 的例子

// 函数返回 never 必须是不可到达的
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
// 判断返回的类型是 never
function fail() {
return error("Something failed");
}

Object

Object 表示非原始类型的类型; 即 不是 number、string、boolean、bigint、symbol、null、undefined 的任何类型
使用 object 类型, 像 Object.create 的 APIs 会更好的表示

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

Type assertions 类型断言

有时候, 会遇到这样一种情况: 你对值的了解比 TypeScript 更多; 通常, 当你知道某些实体的类型可能比其当前类型更具体时, 就会发生这种情况

  • 类型断言是告诉编译器相信我,我知道我在做什么
  • 类型断言类似于其他语言中的类型类型转换, 但不执行任何特殊的数据检查或重组; 它没有运行时影响, 完全由编译器使用; TypeScript 假设已经执行了需要的任何特殊检查

类型断言有两种形式, 一种是 尖括号 语法, 另一个是 as 语法; 这两个形式是相等的, 使用其中一个主要是一种偏好的选择; 但是, 当使用带有 JSX 的 TypeScript 时, 只允许 as 风格的断言

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

用途

  1. 将一个联合类型断言为其中一个类型
  2. 将一个父类断言为更加具体的子类
  3. 将任何一个类型断言为 any
    • 有可能会掩盖真正的类型错误, 所以如果不是特别确定, 就不要使用 as any
    • 不能滥用 as any, 另一方面不要完全否定它的作用, 需要在类型的严格性和开发的便利性之间掌握平衡
  4. 将 any 断言为一个具体的类型
    • 调用之后的返回值断言成一个精确的类型, 方便后续操作
  5. 要使得 A 能够被断言为 B, 只需要 A 兼容 B 或 B 兼容 A 即可(为了在类型断言时的安全考虑, 毫无根据的断言是非常危险的)

TS 式结构类型系统, 类型之间的对比只会比较他们最终的结构, 而忽略定义时的关系

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
interface Animal { name: string }
interface Cat {
name: string;
run(): void;
}
let tom: Cat = {
name: 'tom',
run: () => { console.log('run') }
};
let animal: Animal = tom;
// Cat 包含了 Animal 中所有属性, 此外还有一个额外的方法 run
// TS 并不关心之间的关系, 只会看他们最终的结构有什么关系, 与 Cat extends Animal 等价
interface Animal { name: string }
interface Cat extends Animal { run(): void }
// 当 Animal 兼容 Cat 时, 就可以互相进行类型断言了
interface Animal { name: string }
interface Cat {
name: string;
run(): void;
}
function testAnimal(animal: Animal) {
return (animal as Cat)
}
function testCat(cat: Cat) {
return (cat as Animal)
}

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个

类型别名与字符串字面量类型都是使用 type 进行定义

1
2
3
4
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {}
handleEvent(document.getElementById('Hello'), 'click'); // OK
handleEvent(document.getElementById('Hello'), 'dblclick'); // error TS2345

基本类型

Boolean 布尔值

1
2
3
4
5
6
7
8
9
10
11
12
let isDone: boolean = false;
let isTrue: boolean = true;

// 注意: 使用构造函数 Boolean 创造的对象不是布尔值
let createByNewBoolean: boolean = new Boolean(1)
// Type 'Boolean' is not assignable to type 'boolean'.

// new Boolean() 返回的是一个 Boolean 对象
let createByBoolean: Boolean = new Boolean(1)

// 直接调用 Boolean 也可以返回一个 boolean 类型
let createByBoolean: boolean = Boolean(1)

在 TypeScript 中, boolean 是 JS 中的基本类型, 而 Boolean 是 JS 中的构造函数, 其他基本类型(除了 null、undefined)也是一样的

Number 数值

1
2
3
4
5
6
7
8
9
10
11
12
// 所有的 number 都是浮点值, 支持以下进制
// 二进制
let binary: number = 0b1010;
// 八进制
let octal: number = 0o744;
// 十进制
let decimal: number = 6;
// 十六进制
let hex: number = 0xf00d;

let notANumber: number = NaN;
let infinityNumber: number = Infinity;

String 字符串

1
2
3
4
// 可以使用 单引号、双引号、反引号
let single: string = 'single';
let double: string = "double";
let backtick: string = `backtick ${name}`;

Void

void 就像 any 的相反面: void 是没有, any 就是所有; 没有返回值的函数就可以认为是 void 类型(通常作为函数的返回值), 表示没有任何返回值的函数

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

声明类型的变量 void 没有用, 因为只能分配 null(仅在 --strictNullChecks 未指定的情况下) 或 undefined 分配给它们

不建议声明一个变量是 void 类型, 因为这个变量就只能赋值 undefined 或 null

1
2
let unusable: void = undefined;
unusable = null; // OK if `--strictNullChecks` is not given

Null & Undefined

在 TypeScript 中, undefined 和 null 实际上都有各自的类型, 分别命名为 undefined 和 null; 就像 void 一样, 它们本身并不是特别有用

1
2
let u: undefined = undefined;
let n: null = null;

默认情况下, null 和 undefined 是所有其他类型的子类型; 这意味着可以将 null 和 undefined 赋值给 number 等类型;

1
2
3
4
5
6
7
8
// 不会报错
let num: number = undefined;
let u: undefined;
let num: number = u;

// 但是 void 类型的变量不能赋值给 number
let u: void;
let num: number = u; // Type 'void' is not assignable to type 'number'.

但是, 当使用 --strictNullChecks 标志时, null 和 undefined 仅可分配给任何类型及其各自的类型(一个例外是 undefined 也可分配给 void); 这有助于避免许多常见错误; 如果要传递 string 或 null 或 undefined,则可以使用联合类型 string | null | undefined

建议尽可能使用 --strictNullChecks

Any

当需要描述编写应用程序时不知道的变量类型, 这些值可能来自动态内容, 例如来自用户/第三方库; 在这些情况下, 要选择退出类型检查, 并让值通过编译时的检查, 因此使用 any 类型来标记它们(不做类型校验, 谨慎使用), 表示允许赋值为任意类型

1
let notSure: any = 4;

使用 any 类型是处理已有 JavaScript 代码是一中强大的工作方式, 可以用它来逐渐增加或减少在编译过程中的类型检查; 就像其他编程语言那样, 你可能期望使用 Object 来实现这个功能, 但是注意在 JavaScript , Object 类型仅仅允许分配任意值给他, 但不能调用它的存在或可能的任何方法

声明一个变量为任意值之后, 对它的任何操作、返回的内容的类型都是任意值

1
2
3
4
5
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
3
// 例如: 有一个数组, 但是数组中混合了不同类型
let list: any[] = [1, true, '111'];
list[1] = 100;

未声明类型的变量: 变量如果在声明的时候, 未指定其类型, 那它会被识别为任意值类型

1
2
3
4
let something; // => 等价于 let something: any
something = '???'
something = 777
something.setName('mars')

Array 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 写法一: 在数组元素类型后面添加'[]'来表示这是一个该类型的数组「类型 + 方括号」
// 数组的项中不允许出现其他的类型, 数组中一些方法的参数也会根据数组在定义时约定的类型进行限制
let list: number[] = [1, 2, 3]
let list: number[] = [1, '1', 2, 3] // error
list.push(8) // error

// 写法二: 使用一种通用的数组类型表示,Array<数组元素类型>「数组泛型」
// 数组有两个表示方式, T[] 和 Array<T>
let list: Array<number> = [1, 2, 3]

// 写法三: 用接口表示数组
interface NumberArray {
[index: number]: number;
}
let list: NumberArray = [1, 2, 3];

// 可以使用 any 来表示数组中允许出现任意类型
let list: any[] = ['a', 1, {c: '2'}]

类数组不是数组类型, 不能用普通的方式来描述, 而应该用接口

1
2
3
4
5
6
7
8
9
10
11
function sum() {
let args: number[] = arguments;
} // error

function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
} // ok

实际上常用的类数组都有自己的接口定义, 如 IArguments, NodeList, HTMLCollection 等, 其中 IArguments 是 TypeScript 定义好的类型(内置对象)

1
2
3
4
5
6
7
8
9
function sum() {
let args: IArguments = arguments;
}

interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}

Interfaces 接口

  • 用于描述(对象的)多个形状(shapes), 比如对象、函数、可索引类型、类等, interfaces 是无序的
  • 定义对象类型, 对行为的抽象, 但是具体如何行动需要由类(classes) 去实现(implement)
  • 接口一般首字母大写, 有些还会建议接口名称加上 I 前缀

TypeScript 的核心原则之一是类型检查, 重点在于值的“形状”, 有时称为 “动态类型(duck typing)” 或 “结构子类型化(structural subtyping)”; 在 TypeScript 中, 接口充当命名这些类型的角色, 并且是定义代码内契约及项目外代码契约的有效方法

类型检查器不要求这些属性以任何顺序出现, 只要求接口需要的属性出现并具有所需的类型(约束变量的 shapes 必须和接口的 shapes 一致: 不能少属性, 也不能多属性) 赋值的时候, 变量的形状必须和接口的形状保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Personal {
// 正常写法
name: string;

// 可选属性(Optional Properties), 某些属性只在特定场景下需要
age?: number; // 和正常写法相似, 只是在属性名后多了一个 “?”, 优点是可以描述这些可能可用的属性, 同时还可以防止使用不属于接口的属性

// 只读属性(Readonly properties), 某些属性不希望被修改
// 只允许出现在属性声明、索引签名、构造函数中, 如果和其他访问修饰符同时存在的话, 需要写在其后面
// 只读的约束在于第一次给对象赋值的时候, 而不是第一次给只读属性赋值的时候
readonly sex: string; // 属性只能在首次创建对象时才可以修改就需要使用只读属性

// 额外/任意属性
// 注: 一旦定义了任意属性, 那么确定属性和可选属性的类型都必须是它的类型的子集, 所以一般会用 any
[propName: string]: any;
}
// 多余的属性检查
function Example(config: Personal): { fav: string; dis: string } {
// ...
}

TypeScript 提供了一个 ReadonlyArray<T> 类型, 它与 Array<T> 相同, 所有的可变方法都被删除了, 因此可以确保在创建之后不会更改数组; 即使将整个 ReadonlyArray 分配回普通数组也是非法的, 不过仍然可以使用类型断言来覆盖它

1
2
3
4
5
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro.push(5); // error
a = ro; // error
a = ro as number[]; // ok

方法类型

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}

可索引类型

1
2
3
4
5
6
7
8
9
10
11
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}

interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory";

class 类型、interface 扩展、混合类型、interface 扩展 class…

Classes 类

默认为 pulic

1
2
3
4
5
6
7
8
class Animal {
// 默认情况下, 每个成员都是公共的
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

理解 private

1
2
3
4
5
6
class Animal {
// 当成员被标记为私有时,不能从其包含的类外部访问它
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;

理解 protected

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
34
35
36
37
// 受保护修饰符的作用与私有修饰符非常相似, 但声明受保护的成员也可以在类的实例中访问
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); // error

// 构造函数也可以标记为 protected; 这意味着类不能在其包含的类之外实例化, 但是可以扩展
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend 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"); // Error: The 'Person' constructor is protected

抽象类: 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
// 抽象类是可以从中派生其他类的基类, 它们可能无法直接实例化; 与接口不同, 抽象类可能包含其成员的实现详细信息
// 抽象类中标记为抽象的方法不包含实现, 必须在派生类中实现
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name: " + this.name);

abstract printMeeting(): void; // must be implemented in derived classes
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // constructors in derived classes must call super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type

类和接口

类实现接口

实现(implements) 是面向对象中的一个重要概念; 一般来说, 一个类只能继承自另一个类, 有时候不同类之前可以有一些共有的特性, 这时候可以把特性提取成接口, 用 implements 关键字实现, 这个特性大大提高了面向对象的灵活性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Alarm { alert(): void }
interface Light {
lightOn(): void;
lightOff(): void;
}
interface Door {}
class SecurityDoor extends Door implements Alarm {
alert() { console.log('it\'s SecurityDoor alert') }
}
// 一个类也可实现多个接口
class Car implements Alarm, Light {
alert() { console.log('it\'s Car alert') }
lightOn() { console.log('it\'s Car light on') }
loghtOff() { console.log('it\'s Car light off') }
}

接口继承接口

接口之间也可以是继承关系

1
2
3
4
5
6
interface Alarm { alert(): void }
// LightableAlarm 继承了 Alarm, 除了有 alert 方法, 还有两个新方法
interface LightableAlarm extends Alarm {
lightOn(): void;
lightOff(): void;
}

接口继承类

常见的面向对象语言中, 接口是不能继承类的, 但是在 TS 中却可以

1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Ponit3d extends Point { z: number }
const ponit3d: Point3d = { x: 1, y: 2, z: 3 }

实际上, 当声明 class Point 时, 除了会创建一个名为 Point 的类之外, 同时也创建了一个名为 Point 的类型(实例的类型)

所以既可以将 Point 当作一个类来用(使用 new Point 创建实例)

1
2
3
4
5
6
7
8
9
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const p = new Point(1, 2)

也可以将 Point 当作一个类型来使用(使用 : Point 表示参数的类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
function printPoint(p: Point) { console.log(p.x, p.y) }
printPoint(new Point(1, 2))

// 等价于
class Point { ... }
interface PointInstanceType {
x: number;
y: number;
}
function printPoint(p: PointInstanceType) { console.log(p.x, p.y) }
printPoint(new Point(1, 2))

所以, 回到 Point3d 的例子, 可以明白为什么 TS 会支持接口继承类

1
2
3
4
5
6
7
8
class Point { ... }
interface PointInstanceType {
x: number;
y: number;
}
// 等价于 interface Point3d extends PointInstanceType
interface Point3d extends Point { z: number }
const point3d: Point3d = { x: 1, y: 2, z: 3 }

当声明 interface Point3d extends Point 时, Point3d 继承的实际上是 Point 的实例类型, 也可以理解为定义了一个接口 Point3d 继承另一个接口 PointInstanceType, 所以「接口继承类」和「接口继承接口」没有什么本质区别

注意: PointInstanceType 相较于 Point, 缺少了 constructor 方法, 这是因为声明 Point 类时创建的 Point 类型是不包含构造函数的, 另外, 除了构造函数是不包含的, 静态属性/方法也是不包含的, 换句话说, 声明 Point 类时创建的 Point 类型只包含其中的实例属性和实例方法

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
class Point {
// 静态属性
static origin = new Point(0, 0);
// 静态方法
static distanceToOrigin(p: Point) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
// 实例属性
x: number;
// 实例属性
y: number;
// 构造函数
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
// 实例方法
printPoint() { console.log(this.x, this.y) }
}

interface PointInstanceType {
x: number;
y: number;
printPoint(): void;
}

// 类型 Point 和 PointInstanceType 是等价的
// 同样的, 在接口继承类的时候, 也只会继承它的实例属性和方法
let p1: Point;
let p2: PointInstanceType;

Functions 方法

函数是 JS 中的一等公民

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
// 可以向每个参数添加类型, 然后向函数本身添加返回类型;
// TypeScript 可以通过查看 return 语句来确定返回类型, 所以在很多情况下, 我们也可以选择不使用它

// 函数声明(Function Declaration)
// 注: 输入多余的(或少于要求的)参数, 是不被允许的
function add(x: number, y: number): number { return x + y }

// 函数表达式(Function Expression)
let add = function(x: number, y: number): number { return x + y }
// 如果需要手动给函数表达式添加类型(不要混淆 TS 和 ES6 中的 =>)
// TS 中的 => 表示函数的定义, 左边是输入类型需要用括号括起来, 右边是输出类型
let add: (x: number, y: number) => number = function (x: number, y: number): number { return x + y }

// 可选&默认参数
// 注: 可选参数后面不允许出现必须参数
function buildName(firstName = "Will", lastName?: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // ’Bob'
let result2 = buildName("Bob", "Adams"); // Bob Adams
let result3 = buildName(null, "Adams"); // Will Adams
let result4 = buildName("Bob", "Adams", "Sr."); // error

// 剩余参数
function push(array: [], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
})
}
let a = [];
push(a, 1, 2, 3);

也可以使用接口定义函数的形状, 采用函数表达式|接口定义函数的方法时, 对等号左侧进行类型限制, 可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变

1
2
3
4
5
6
7
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}

重载允许一个函数接受不同数量或类型的参数时, 做出不同的处理

注: TypeScript 会优先从最前面的函数定义开始匹配, 所以多个函数定义如果有包含关系, 需要优先把精确的定义写在前面

利用联合类型, 可以这么实现

1
2
3
4
5
6
7
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}

但是这样有一个缺点, 就是不能够精确的表达, 输入为数字的时候, 输出也应该为数字, 输入为字符串的时候, 输出也应该是字符串

1
2
3
4
5
6
7
8
9
10
// 这是可以用重载定义多个 reverse 的函数类型
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}

Generics 泛型

用于创建可复用组件的重要工具之一, 即能够创建可在多种类型而不是单个类型上工作的组件; 使用户可以使用这些组件并使用自己的类型

在定义函数、接口或类的时候, 不预先制定具体的类型, 而在使用的时候再指定类型的一种特性

泛型的重要元素: 恒等函数(the identity function: 一个函数, 它将返回传入的任何内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不使用泛型时, 需要给恒等函数特定的类型
// 也可以使用 any 类型来描述函数; 虽然使用 any 类型是通用的, 它会使函数接受所有类型, 但是当函数返回时, 会丢失关于该类型的信息
// 缺点: 没有准准的定义返回值类型
function identity(arg: number): number {
return arg;
}
// 也可以使用 any 类型来描述函数;
// 虽然使用 any 类型是通用的, 它会使函数接受所有类型
// 但是当函数返回时, 会丢失关于该类型的信息
function identity(arg: any): any {
return arg;
}
// 所以我们需要一种捕获参数类型的方法, 方便使用它来表示返回的内容, 使用类型变量(一种特殊类型的变量, 它作用于类型而不是值)
// 给恒等函数添加了一个 类型变量 T, T 允许捕获用户提供的类型, 方便以后使用该信息, 再次使用 T 作为返回类型
// 在检查时, 会看到参数和返回类型使用了相同类型, 这使我们能够在函数一边接受类型信息, 而在另一边传送
// 所以这个恒等函数是通用的, 因为它适用于多种类型; 与使用任何数字不同, 它与第一个使用数字作为参数和返回类型的身份函数一样精确(即它不会丢失任何信息)
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString")
// 该方法比上面的方法常见; 使用参数推断, 希望编译器根据传入参数的类型自动设置 T 的值
let output = identity("myString")

开始使用泛型时, 当创建诸如 identity 之类的泛型函数时, 编译器将强制您正确使用函数主体中的任何泛型类型的参数; 也就是说, 实际上将这些参数视为可以是任何和所有类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function identity<T>(arg: T): T {
console.log(arg.length) // Error: T doesn't have .length
return arg;
}
// 因为类型变量代表任何类型, 所以如果打算将这个函数用作 T 的数组, 而不是直接对 T 的数组, 就需要像创建其他类型的的数组那样描述它
function identity<T>(arg: T[]): T {
console.log(arg.length) // OK
return arg;
}
// ts 允许我们使用泛型类型变量 T 作为正在使用的类型的一部分, 而不是整个类型, 从而提供了更大的灵活性
// 也可以这样编写实例
function identity<T>(arg: <array>T): <array>T {
console.log(arg.length) // OK
return arg;
}

泛型类型

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
// 泛型函数的类型与非泛型函数的类型类似, 首先列出类型参数, 类似于函数声明
function identity<T>(arg: T): T {
return arg;
}
// 只要类型变量的数量和类型变量的使用方式一致, 我们还可以为类型中的泛型类型参数使用不同的名称
// 两者没有区别, 只是名称不同而已, 可以根据自己需求来定
let myIdentity: <T>(arg: T) => T = identity;
let myIdentity: <U>(arg: U) => U = identity;
// 也可以将泛型类型编写为对象文字类型的调用签名
let myIdentity: { <T>(arg: T): T } = identity;

// 结合 interface 写出通用接口
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;

// 将泛型参数移动为整个接口的参数, 使得类型参数对接口的所有其他成员可见
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

多个类型参数

1
2
3
4
5
// 定义泛型时, 可以一次定义多个类型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
swap([7, 'seven']); // ['seven', 7]

泛型约束: 在函数内部使用泛型变量时, 由于事先不知道是哪种类型, 所以不能随意操作它的属性或方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
} // error TS2339 因为 T 不一定包含属性 length

// 这时需要对泛型进行约束, 只允许这个函数传入哪些包含 length 属性的变量, 这就是泛型约束
interface Lengthwise { length: number }
// 使用 extends 约束泛型 T 必须符合接口 Lengthwise 形状, 就是必须包含 length 属性
// 如果调用时传入的 arg 不包含 length, 那么在编译阶段就会报错
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

// 多个类型参数之间也可以相互约束
// 使用两个类型参数, 其中要求 T 继承 U, 这样就保证 U 上不会出现 T 中不存在的字段
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 })

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}

// 注意: 此时使用泛型接口的时候, 需要定义泛型的类型
let createArray: CreateArrayFunc<any>;
createArray = function <T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']

泛型类

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
34
// 通用类具有与通用接口相似的形状; 泛型类在类名称后的尖括号(<>)中具有泛型类型参数列表
// 可以使用字符串或者更复杂的对象
// 与接口一样, 将类型参数放在类本身上可以确保类的所有属性都使用相同的类型
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; };

// Example
interface IProps {
foo: string;
}
interface IState {
loading: boolean;
}
class MyComponent extends React.Component<IProps, IState> {
render() {
return <span>{this.props.foo}</span>
}
}
// Example2
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!

泛型参数的默认类型

1
2
3
4
5
6
7
8
// TS 2.3 后, 可以为泛型中的类型参数指定默认类型
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;ß
}

Enums 枚举

枚举允许定义一组命名常量, 使用枚举可以更容易记录意图, 或者创建一组不同的案例; TS 提供了基于 number 和 string 的枚举

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
34
35
// number
// 数值依次递增, 不关心成员的值本身, 但关心在同一个枚举中的值不同
enum Direction {
Up, // 0
Right, // 1
Down, // 2
Left // 3
}
let directions = [Directions.Up, Directions.Right, Directions.Down, Directions.Left, ];
// 编译结果
var directions = [0 /* Up */, 1 /* Right */, 2 /* Down */, 3 /* Left */];
// 使用枚举: 只需作为 enum 本身的属性访问任何成员, 并使用 enum 的名称声明类型
enum Response {
No = 1,
Yes = 2
}
function respond(recipient: string, message: Response): void {}
respond("Hello World", Response.Yse)
// 注意: 数值枚举可以混合在计算成员和常量成员中(没有初始化程序的枚举要么需要首先使用,要么必须在用数字常量或其他常量枚举成员初始化的数字枚举之后出现)
enum E {
A = getSomeValue(),
B, // Error! Enum member must have initializer.
}

// string
// 字符串枚举是一个类似的概念, 但是在运行时方面有一些细微的差别;
// 在字符串枚举中, 每个成员都必须使用字符串文字或另一个字符串枚举成员进行常量初始化
// 虽然字符串枚举没有自动递增的行为,但字符串枚举的好处是可以很好地“序列化”
// 字符串枚举允许在代码运行时提供有意义且可读的值, 而与枚举成员本身的名称无关
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

Type Inference 类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 常见的类型
let zoo = [new Rhino(), new Elephant(), new Snake()]
// 将 zoo 推断为一个 Animal[]; 显式的提供类型推断
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]

// 上下文类型, 根据上下文推断事物的类型
// 适用范围: 常见的情况包括函数调用的参数、赋值的右侧、类型断言、对象和数组文字的成员及 return 语句; 上下文类型还可以充当最佳通用类型的候选类型
window.onmousedown = function(mouseEvent) {
// 因为 onmousedown 中包含 button 属性, 但是没有 kangaroo 属性
console.log(mouseEvent.button); //<- OK
console.log(mouseEvent.kangaroo); //<- Error!
};
// 如果函数不在上下文键入的位置, 则该函数的参数将隐式地具有 any 类型,并且不会发出任何错误(除非使用 --noImplicitAny 选项)
const handler = function(uiEvent) {
console.log(uiEvent.button); //<- OK
}
// 还可以显式地将类型信息提供给函数的参数, 以覆盖任何上下文类型
window.onscroll = function(uiEvent: any) {
console.log(uiEvent.button); //<- Now, no error is given; 但是, 这段代码将记录未定义的日志, 因为 uiEvent 没有称为 button 的属性
};

Type Compatibility 类型兼容性

TypeScript 中的类型兼容性基于结构子类型; 结构化类型是一种仅基于其成员关联类型的方式

TypeScript 的类型系统允许某些在编译时未知的操作是安全的; 当类型系统具有该属性时, 它被认为是不靠谱的; TypeScript 允许不合理行为的地方是被仔细考虑过的

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
34
35
36
37
38
// TypeScript 的结构类型系统的基本规则是, 如果 y 与 x 至少具有相同的成员,则 x 与 y 兼容
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;

// 类型系统要求源函数的返回类型必须是目标类型的返回类型的子类型
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});
x = y; // OK
y = x; // Error, 因为 x 缺少 location 这个属性

// 在比较函数参数的类型时, 如果源参数可以分配给目标参数, 则分配成功, 反之亦然; 这是不合理的, 因为调用者最终可能会得到一个采用更专门化类型的函数, 但调用的函数的专门化类型却更少
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) { /* ... */ }
// 不健全, 但是常用&常见
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + "," + e.y));
// 健全, 但不受欢迎
listenEvent(EventType.Mouse, (e: Event) => console.log((e as MouseEvent).x + "," + (e as MouseEvent).y));
listenEvent(EventType.Mouse, ((e: MouseEvent) => console.log(e.x + "," + e.y)) as (e: Event) => void);
// 仍然不允许(明显错误), 对完全不兼容的类型实施类型安全
listenEvent(EventType.Mouse, (e: number) => console.log(e));

// 在比较函数的兼容性时, 可选参数和必需参数是可互换的; 源类型的额外可选参数不是错误, 源类型中没有相应参数的目标类型的可选参数不是错误
// 当一个函数有一个 rest 参数时, 它被看作是无穷多个可选参数
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));

Advanced Types 高级类型

JSX

三斜杠指令

  • 三重斜杠指令是包含单个XML标记的单行注释, 注释的内容用作编译器指令
  • 三重斜杠指令仅在其包含文件的顶部有效
  • 一个三重斜杠指令前只能有单个或多行注释, 包括其他三重斜杠指令
  • 如果在语句或声明之后遇到它们, 它们将被视为常规的单行注释, 没有特殊含义
  • 三斜杠引用指示编译器在编译过程中包含其他文件
    1
    /// <reference path="..." />

类型检查 JavaScript 文件

TypeScript 2.3及以后版本支持在 .js 文件中使用 ——checkJs 类型检查和报告错误

可以通过添加 // @ ts-nocheck 注释来跳过对某些文件的检查; 相反, 通过不设置 --checkJs// @ts-check 注释来选择仅检查几个 .js 文件, 还可以通过在上一行添加 // @ts-ignore 来忽略特定行上的错误

注意, 如果有 tsconfig.json, 则 JS 检查将遵循严格的标志, 例如 noImplicitAny, strictNullChecks 等; 但是, 由于 JS 检查相对松散, 因此将严格的标志与之结合可能会有意想不到的结果

实用程序类型

TypeScript 提供了几个实用程序类型来促进通用类型转换, 这些实用程序在全球都可用

  • Partial<T>
  • Readonly<T>
  • Record<K,T>
  • Pick<T,K>
  • Omit<T,K>
  • Exclude<T,U>
  • Extract<T,U>
  • NonNullable<T>
  • Parameters<T>
  • ConstructorParameters<T>
  • ReturnType<T>
  • InstanceType<T>
  • Required<T>
  • ThisParameterType
  • OmitThisParameter
  • ThisType<T>

关注