基本类型
JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
原始数据类型包括布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol。
对象类型包括数组、函数以及普通对象。
TypeScript 在这些类型的基础上衍生出元组、枚举、any、void、never 等。
TypeScript 的一个比较重要的功能就是类型检查,如果类型不符就会报错。下面对一些简单基本类型的检查进行列举。
布尔值
1 | let isDone: boolean = false |
注意,使用构造函数 Boolean 创造的对象不是布尔值:
1 | let createdByNewBoolean: boolean = new Boolean(1) // 报错 |
事实上 new Boolean() 返回的是一个 Boolean 对象:
1 | let createdByNewBoolean: Boolean = new Boolean(1) |
直接调用 Boolean 也可以返回一个 boolean 类型:
1 | let createdByBoolean: boolean = Boolean(1) |
其他基本类型(除了 null 和 undefined)一样,不再赘述。
数值
1 | let decLiteral: number = 6 |
字符串
1 | let myName: string = 'Tom' |
数组
写法一:「类型 + 方括号」表示法
1 | let fibonacci: number[] = [1, 1, 2, 3, 5] |
写法二:数组泛型
1 | let fibonacci: Array<number> = [1, 1, 2, 3, 5] |
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象:
1 | let tom: [string, number] = ['Tom', 25] |
2.6 版本后,元素个数和类型必须与声明的个数和类型一致。
枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
1 | enum Days { |
也可以给枚举项手动赋值:
1 | enum Days { |
上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增。如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 不会察觉到这一点,后面的值会覆盖前面的值,手动赋值的枚举项也可以为小数或负数。
常数枚举是使用 const enum 定义的枚举类型:
1 | const enum Directions { |
常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员,压缩了代码体积。上例的编译结果是:
1 | var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */] |
any
任意值(Any)用来表示允许赋值为任意类型:
1 | let arr: any[] = [1, 2, 3, 'a', 'b'] |
void
一般用 void 表示没有任何返回值的函数:
1 | function alertName(): void { |
null 和 undefined
在 TypeScript 中,可以使用 null 和 undefined 来定义这两个原始数据类型:
1 | let u: undefined = undefined |
与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:
1 | let num1: number = undefined |
never
never 一般用于抛出异常或陷入死循环的情况:
1 | const errorFunc = (massage: string): never => { |
1 | const infiniteFunc = (): never => { |
object
1 | let o: object = {} |
类型断言
如果没有明确的指定类型,那么 TypeScript 会依照类型断言(Type Inference)的规则推断出一个类型。
1 | let myFavoriteNumber = 'seven' |
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查。
1 | let myFavoriteNumber |
联合类型
联合类型(Union Types)表示取值可以为多种类型中的一种。
1 | let myFavoriteNumber: string | number |
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
1 | function getLength(something: string | number): number { |
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
1 | let myFavoriteNumber: string | number |
接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
简单的例子
1 | interface IPerson { |
定义的变量比接口少了一些属性是不允许的,多一些属性也是不允许的。赋值的时候,变量的形状必须和接口的形状保持一致。
可选属性
1 | interface IPerson { |
任意属性
1 | interface IPerson { |
只读属性
1 | interface IPerson { |
注意,只读属性必须在对象赋值时进行赋值。
接口的继承
1 | interface IVegetables { |
小应用:计数器
3.1 版本后,支持了直接给函数添加属性
1 | interface ICounter { |
函数
简单的例子
1 | // 函数声明(Function Declaration) |
注意,输入多余的(或者少于要求的)参数,是不被允许的。
对于函数表达式,注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。等号左边的 mySum 可以通过赋值操作进行类型推论而推断出来,故可简写为:
1 | let mySum = function (x: number, y: number): number { |
用接口定义函数的形状
采用接口定义函数时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变:
1 | interface SearchFunc { |
用类型别名定义函数的形状
类型别名用来给一个类型起个新名字:
1 | type SearchFunc = (source: string, subString: string) => boolean |
可选参数
接口中的可选属性类似,用 ? 表示可选的参数:
1 | function buildName(firstName: string, lastName?: string) { |
需要注意的是,可选参数必须接在必需参数后面。
参数默认值
在 ES6 中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数:
1 | function buildName(firstName: string, lastName: string = 'Cat') { |
此时就不受「可选参数必须接在必需参数后面」的限制了。
剩余参数
ES6 中,可以使用 …rest 的方式获取函数中的剩余参数(rest 参数),所以我们可以用数组的类型来定义 rest:
1 | function push(array: any[], ...items: any[]) { |
注意,rest 参数只能是最后一个参数。
重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理 :
1 | function reverse(x: number): number |
上例中,重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
简单的例子
1 | function createArray<T>(length: number, value: T): Array<T> { |
在函数名后添加了 <T>
,其中 T 用来指代任意输入的类型,在后面的输入 value: T
和输出 Array<T>
中即可使用,保证了 value 与 Array 类型一致。
多个类型参数
1 | function swap<T, U>(tuple: [T, U]): [U, T] { |
泛型约束
1 | interface Lengthwise { |
上例中使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。这样写可以使函数中使用 length 属性时不报错,同时也可以为 T 的类型作约束。多个类型参数之间也可以互相约束:
1 | function copyFields<T extends U, U>(target: T, source: U): T { |
泛型接口
1 | interface CreateArrayFunc { |
也可以把泛型参数提前到接口名上,此时在使用泛型接口的时候,需要定义泛型的类型:
1 | interface CreateArrayFunc<T> { |
泛型参数的默认类型
2.3 以后,可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用:
1 | function createArray<T = string>(length: number, value: T): Array<T> { |
类
public private 和 protected
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected:
- public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的。
- private 修饰的属性或方法是私有的,不能在声明它的类的外部访问。
- protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的。
需要注意的是:
- TypeScript 编译之后的代码中,并没有限制 private 属性在外部的可访问性。
- 当构造函数修饰为 private 时,该类不允许被继承或者实例化。
- 当构造函数修饰为 protected 时,该类只允许被继承。
- 修饰符和 readonly 可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值。如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。
抽象类
abstract 用于定义抽象类和其中的抽象方法。抽象类不允许被实例化,抽象类中的抽象方法必须被子类实现:
1 | abstract class Animal { |
需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类。
类与接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。
1 | interface Alarm { |
一个类可以实现多个接口,接口可以继承接口。接口甚至也可以继承类:
1 | class Point { |
因为当声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(TS 中最重要的类型检查功能中的类型)。
高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
1 | interface A { |
类型断言和类型保护
有时需要强行给某个字段或方法设定类型以使用对应的属性,就要使用类型断言。有两种语法<类型>值
和 值 as 类型
:
1 | let someValue: any = 'this is a string' |
使用类型断言,需要多次判断十分麻烦。所以使用类型保护。类型保护是param is SomeType
的形式,用来明确一个联合类型变量的具体类型:
1 | function isFish(pet: Fish | Bird): pet is Fish { |
typeof 类型保护用于 number, string, boolean, symbol:
1 | if (typeof v === 'string') { |
索引类型
keyof 关键字是索引类型查询操作符,能够获得任何类型上已知的公共属性名(非 never、undefined、null 类型)的联合:
1 | interface P { |
映射类型
TypeScript 提供了从旧类型中创建新类型的一种方式。在映射类型里新类型以相同的形式去转换旧类型里每个属性:
1 | interface Person { |
结果如下:
1 | interface PersonPartial { |
字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个:
1 | type EventNames = 'click' | 'scroll' | 'mousemove' |
unknown
使用 any 类型,就无法享受 TypeScript 大量的保护机制。3.0 引入了新的 unknown 类型,它是 any 类型对应的安全类型。
- unknown 类型只能被赋值给 any 类型和 unknown 类型本身。
- 在交叉类型中,任何类型都可以吸收 unknown 类型。这意味着将任何类型与 unknown 相交不会改变结果类型。
- 在联合类型中,unknown 类型会吸收任何类型。这就意味着如果任一组成类型是 unknown,联合类型也会相当于 unknown。
- never 是 unknown 的子类型。
- 类型为 unknown 的值上使用的运算符只有相等或不等。
条件类型
2.8 版本后开始支持条件类型,增加了语言的灵活性。形如T extends U ? X : Y
,若类型 T 可被赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型。
1 | type TypeName<T> = T extends string |
分步式条件类型:(A | B) extends U ? X : Y
,等价于(A extends U ? X : Y) | (B extends U ? X : Y)
。
杂项
命名空间
命名空间是一种内部模块,同文件的其他地方或其他文件只能访问到该空间的导出内容。下面是同文件引用的一个例子:
1 | namespace Validation { |
声明合并
相同名称的接口会合并。不同接口中如有相同属性,后出现的属性会覆盖先出现的属性。
命名空间可以和同名命名空间、同名类、同名函数、同名枚举合并。同名类和同名函数需置于命名空间前。
声明文件
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 script 标签引入 jQuery,然后就可以使用全局变量 $ 或 jQuery 了。但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么,这时就需要声明语句。
1 | declare var jQuery: (selector: string) => any |
上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。其他类型的声明有:declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface
和 type
声明全局类型export
导出变量export namespace
导出(含有子属性的)对象export default
ES6 默认导出export =
commonjs 导出模块export as namespace
UMD 库声明全局变量declare global
扩展全局变量declare module
扩展模块/// <reference />
三斜线指令
通常会把声明语句放到一个单独的文件(.d.ts 为后缀)中。一般来说,ts 会解析项目中所有的 _.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当 .d.ts 放到项目中时,其他所有 _.ts 文件就都可以获得类型定义。
tsconfig.json 配置
在 TS 的项目中,TS 最终都会被编译 JS 文件执行,TS 编译器在编译 TS 文件的时候都会根据项目根目录 tsconfig.json 文件的配置进行编译:
files
表示编译需要编译的单个文件列表
1 | "files": [ |
include
表示编译需要编译的文件或目录
1 | "include": [ |
exclude
表示编译器需要排除的文件或文件夹
1 | "exclude": [ |
extends
引入其他配置文件,继承配置
1 | // 把基础配置抽离成tsconfig.base.json文件,然后引入 |
compileOnSave
设置保存文件的时候自动编译(vscode 暂不支持)
1 | "compileOnSave": true |
references
指定工程引用依赖
compilerOptions
配置编译选项
编译配置选项compilerOptions
包括:
1 | "compilerOptions": { |