《TypeScript Deep Dive》
- Why TS?
- Provide an optional type system for JavaScript 提供可选的类型系统给js
- Provide planned features from future JavaScript editions to current JavaScript engines 提供未来 js 版本的计划特性给现在的 js 引擎
- Why type system?——enhance code quality and understandability 增强代码质量与可理解性
- increase your agility when doing refactoring 重构时提高敏捷性
一、类型系统
1 类型注解
1.1 原始类型
- string:
let name: string = "bob";可使用反引号定义模板字符串${expr} - number:
let dec: number = 6;let binary: number = 0b1010;可表示二、八进制 - boolean:
let isDone: boolean = false; - null、undefined: 能被赋值给任意类型的变量,默认是所有类型的子类型
1.2 特殊类型
- any: 兼容所有类型,所有类型可赋值给它,它也能赋值其他任何类型
- 注意:变量声明时未指定其类型,则会被识别为任意值类
- void: 与 any 类型相反,表示无任何类型,只能被赋值为 null、undefined
1.2 接口(对象类型)
- 作用:对类的一部分行为进行抽象或对「对象的形状(Shape)」进行描述,具体如何行动由类去实现
- 特点:赋值时变量的形状必须和接口的形状保持一致、可扩展 extends、可被类实现 implements
- 属性
- 可选属性
prop?: - 任意属性
[propName:string]:any - 只读属性
readonly prop:
- 可选属性
- 注意:一旦定义任意属性,则确定属性、可选属性的类型都必须是其类型的子集
interface Name {first: string;second: string;}let name: Name;name = {first: "John",second: "Doe"};name = {// Error: 'Second is missing'first: "John"};
1.3 数组类型
- 元素类型接后缀
[]:let boolArray: boolean[] = [true,false] - 数组泛型
Array<elemType>可用来表示数组
1.4 元组类型(Tuple)
- 使用:由
:[typeofmember1, typeofmember2]为元组添加类型注解 - 特点:数组合并了相同类型的对象,元组合并了不同类型的对象
- 注意:
- 直接对元组类型变量进行初始化或赋值时,需提供所有元组类型中指定的项
- 当添加越界的元素时,类型会被限制为元组中每个类型的联合类型
1.5 函数类型
- 函数声明
function sum(x: number, y: number): number {return x + y;}
- 函数表达式:在 TypeScript 的类型定义中,
=>用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
let mySum: (x: number, y: number) => number = function(x: number,y: number): number {return x + y;};
- 接口定义函数的形状
interface SearchFunc {(source: string, substring: string): boolean;}let mySearch: SearchFunc;mySearch = function(source: string, substring: string) {return source.search(substring) !== -1;};
- 可选参数:
?表示可选的参数,一般可选参数后不允许出现必需参数 - 参数默认值:添加默认值的参数会被识别为可选参数,后可接必需参数
- 重载:允许一个函数接受不同数量或类型的参数时,作出不同的处理
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("");}}//重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现
1.6 泛型(generic)
- 特点:定义函数、接口或类的时候,不预先指定具体的类型,使用时再指定类型
- 类型参数:
<T>捕获用户传入的类型,以便使用- 可一次定义多个类型参数,如
<T,U> - 默认类型:可指定为
<T = string>,没有在代码中直接指定类型参数,从实际值参数中也无法推测出时起作用
- 可一次定义多个类型参数,如
- 泛型函数
function identity<T>(arg: T): T {return arg;}function createArray<T>(length: number, value: T): Array<T> {//泛型变量,如T[]let result: T[] = [];for (let i = 0; i < length; i++) {result[i] = value;}return result;}createArray(3, 'x'); // ['x', 'x', 'x']
- 泛型约束:约束泛型的形状等,多个类型参数之间也可以互相约束
interface Lengthwise {length: number;}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length);return arg;}
- 泛型接口:使用含有泛型的接口来定义函数的形状
interface CreateArrayFunc {<T>(length: number, value: T): Array<T>;}let createArray: CreateArrayFunc;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']
- 泛型类
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; };
1.7 联合类型(union types)
- 使用
|作为标记,表示取值可以为多种类型之一,如string | number - 当不确定联合类型的变量具体类型时,只能访问其所有类型共有的属性或方法
1.8 交叉类型(intersection types)
- 使用
&作为标记,将多个类型合并为一个类型,包含所需的所有类型的特性 - extend 模式可从两个对象中创建一个新对象,新对象拥有两个对象所有的功能
function extend<T, U>(first: T, second: U): T & U {const result = <T & U>{};for (let id in first) {(<T>result)[id] = first[id];}for (let id in second) {if (!result.hasOwnProperty(id)) {(<U>result)[id] = second[id];}}return result;}const x = extend({ a: "hello" }, { b: 42 });// 现在 x 拥有了 a 属性与 b 属性const a = x.a;const b = x.b;
1.9 类型别名
- 使用
type SomeName = someValidTypeAnnotation的语法来创建别名 - 与接口区别:
- 需要使用类型注解的层次结构,请使用接口(能使用 implements、extends)
- 需要给联合类型和交叉类型提供一个语义化的名称时,请使用类型别名
type Text = string | { text: string };type Coordinates = [number, number];type Callback = (data: string) => void;
2 从 JS 迁移
2.1 使用 any 类型减少错误
2.2 引用第三方库的声明文件
2.2.1 声明语句
- 声明语句中只能定义类型,切勿在声明语句中定义具体的实现
- 声明全局变量:
declare var、declare let、declare const(常用) - 声明全局方法:
declare function - 声明全局类:
declare class - 声明全局枚举类型:
declare enum - 声明含有子属性的全局对象:
declare namespace- ES6 使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间
- 声明全局接口或类型:
interface、type
2.2.2 声明文件
- 定义:由
.d.ts为后缀的文件 - 管理:使用
@types统一管理第三方库的声明文件,存在于DefinitelyTyped仓库- 如
npm install @types/jquery --save-dev
- 如
- 全局变量:通过
<script>标签引入第三方库,注入全局变量- 以
npm install @types/xxx --save-dev安装的,则不需要任何配置 - 否则书写声明文件并存放于当前项目
src目录下(或对应的源码目录下)
- 以
// src/jQuery.d.ts//声明合并declare function jQuery(selector: string): any;declare namespace jQuery {function ajax(url: string, settings?: any): void;const version: number;class Event {blur(eventType: EventType): void;}enum EventType {CustomClick}namespace fn {function extend(object: any): void;}interface AjaxSettings {method?: "GET" | "POST";data?: any;}}// src/index.tsjQuery('#foo');jQuery.ajax('/api/get_something');
npm包:通过import foo from 'foo'导入,符合ES6模块规范- 检查是否有声明文件
- package.json 中有 types 字段,或者有一个 index.d.ts 声明文件
- 尝试安装一下对应的
@types包npm install @types/foo --save-dev
- 否则书写声明文件,存放于
types/foo/index.d.ts中,同时配置tsconfig.json中的paths、baseUrl字段 - 注意:
export defaultES6默认导出,可用import foo from 'foo'而非import { foo } from 'foo'来导入这个默认值。只有 function、class 和 interface 可以直接默认导出,其他的变量需要先定义出来,再默认导出
- 检查是否有声明文件
// types/foo/index.d.tsexport namespace foo {const name: string;namespace bar {function baz(): string;}}// src/index.tsimport { foo } from 'foo';console.log(foo.name);foo.bar.baz();
- 扩展原有模块的类型
// types/moment-plugin/index.d.tsimport * as moment from 'moment';declare module 'moment' {export function foo(): moment.CalendarKey;}// src/index.tsimport * as moment from 'moment';import 'moment-plugin';moment.foo();
3 内置对象
3.1 ECMAScript 内置对象
- Boolean:
let b: Boolean = new Boolean(1); - Error:
let e: Error = new Error('Error occurred'); - Date:
let d: Date = new Date(); - RegExp:
let r: RegExp = /[a-z]/;
3.2 DOM 和 BOM 的内置对象
- HTMLElement:
let body: HTMLElement = document.body; - NodeList:
let allDiv: NodeList = document.querySelectorAll('div'); - Event:
document.addEventListener('click', function(e: MouseEvent) {// Do something}); - Document
3.3 TypeScript 核心库的定义文件
- 定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的
- TypeScript 核心库的定义中不包含 Node.js 部分
3.4 用 TypeScript 写 Node.js
- 需要引入第三方声明文件:
npm install @types/node --save-dev
4 字符串字面量类型
- 使用 type 定义一个字符串字面量类型 EventNames,只能取三种字符串中的一种
type EventNames = 'click' | 'scroll' | 'mousemove';function handleEvent(ele: Element, event: EventNames) {// do something}handleEvent(document.getElementById('hello'), 'scroll');
5 枚举(enum)
- 用于取值在一定范围内的场景,如一周只有七天,颜色只能为红绿蓝等
- 枚举成员会被赋值为从 0 开始递增的数字,同时对枚举值到枚举名进行反向映射
enum Days{Sun,Mon,Tue,Wed,Thu,Fri,Sat}console.log(Days["Sun"] === 0); // trueconsole.log(Days[0] === "Sun"); // true
- 手动赋值:手动赋值的枚举项可为小数或负数,未手动赋值的枚举项会接着上一个枚举项递增,递增步长为 1
enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};console.log(Days["Sun"] === 7); // trueconsole.log(Days["Mon"] === 1.5); // trueconsole.log(Days["Tue"] === 2.5); // trueconsole.log(Days["Sat"] === 6.5); // true
- 枚举项:常数项、计算所得项
- 常数项:不具有初始化函数并且之前的枚举成员是常数;枚举成员使用常数枚举表达式初始化
- 计算所得项:
enum Color {Red, Green, Blue = "blue".length}; - 注意:紧接在计算所得项后的未手动赋值的项,会因无法获得初始值而报错
- 常数枚举:使用
const enum定义的枚举类型- 特点:与普通枚举区别为,它会在编译阶段被删除,且不能包含计算成员
- 外部枚举:常用于声明文件中,可同时使用
declare、const
declare const enum Directions {Up,Down,Left,Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
6 类(class)
6.1 类的概念
- 类(Class):定义了一件事物的抽象特点,包括它的属性和方法
- 对象(Object):类的实例,通过
new生成 - 面向对象(OOP: Object Oriented Programming)三大特性:封装、继承、多态
- 封装(Encapsulation):对数据的操作隐藏起来,对外只暴露接口
- 继承(Inheritance):子类(派生类)继承父类(超类),拥有更具体的特性
- 多态(Polymorphism):由继承产生的不同类,对同一方法可有不同的响应
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):一些关键字,用于限定成员或类型的性质,如
public - 抽象类(Abstract Class):供其他类继承的基类,不允许被实例化。抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但可以实现多个接口
6.2 ES6中类的用法
- 类的方法:类的所有方法都定义在类的prototype属性上面
- 类的新方法可以添加在prototype对象上面。如
Object.assign方法 - 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
Generator方法:返回迭代器的函数,*为标识,使用yield(产出)语句定义不同的内部状态,实例调用next()方法返回拥有done、value属性的对象static静态方法:只能由类名直接访问,不能被实例继承- 包含的this指向类而非实例
- 父类的静态方法,可以被子类继承,也可从super对象上调用
- 类的新方法可以添加在prototype对象上面。如
class Animal {//ES5中的构造函数即ES6中类的构造方法constructor//constructor方法默认返回实例对象(即this),也可指定返回对象constructor(name) {this.name = name;}sayHi() {return `My name is ${this.name}`;}static isAnimal(a) {return a instanceof Animal;}}Object.assign(Animal.prototype, {toMakeNoise(){},toMove(){}});let a = new Animal('Jack');Animal.isAnimal(a); // true
- 类的继承:
extends关键字实现继承,super关键字调用父类的构造函数和方法;静态方法/属性继承只能通过派生类访问,不能通过派生类的实例访问
class Cat extends Animal {//派生类构造函数里一定要调用super(),且必须在this.xx之前调用constructor(name) {super(name); // 调用父类的 constructor(name)console.log(this.name);}sayHi() {return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()}}let c = new Cat('Tom'); // Tomconsole.log(c.sayHi()); // Meow, My name is Tom
6.3 ES7中类的用法
- 实例属性:除了在construtor中的this.xx定义外,可直接在类里面定义
- 静态属性:使用
static关键字实现静态属性,只能由类名直接访问,不能被实例继承
class Animal {name = 'Jack';static num = 42;constructor() {// ...}}let a = new Animal();console.log(a.name); // Jackconsole.log(Animal.num); // 42
6.4 TypeScript中类的用法
- 修饰符:
public、private、protected- 默认所有属性和方法都是公有的(
public),即可以在任何地方被访问 private修饰的属性或方法是私有的,不能在声明它的类的外部访问protected修饰的属性或方法是受保护的,不能在类外部访问,子类中允许- 参数属性:修饰符还可以使用在构造函数参数中,等同于类中定义该属性
- 如
constructor(public name: string) {}
- 如
- 默认所有属性和方法都是公有的(
class Animal {private name: string;public constructor(theName: string) {this.name = theName;}//构造函数修饰为`private`时,该类不允许被继承或实例化//构造函数修饰为`protected`时,该类只允许被子类继承,不允许被实例化}let a = new Animal('Jack');console.log(a.name); // Jacka.name = 'Tom';//Error:Property 'name' is private ...
- 只读属性:
readonly关键字,只允许出现在属性声明或索引签名中- readonly 和其他访问修饰符同时存在的话,需要写在其后面
class Animal {// public readonly name: string;constructor(public readonly name: string) {}}
- 抽象类:
abstract关键字- 抽象类是不允许被实例化的
- 抽象类中的抽象方法不包含具体实现,且必须在派生类中实现
abstract class Animal {constructor(public name: string) {}abstract sayHi(): void;}class Cat extends Animal {sayHi() {console.log(`Meow, My name is ${this.name}`);}}let cat = new Cat('Tom');
- 类的类型:类似接口,直接
:接类名,如let a: Animal = new Animal('Jack');
6.5 类与接口
6.5.1 类实现接口
- 实现(
implements):不同类之间可以有一些共有的特性,把这些共有特性提取成接口(interfaces),用implements关键字来实现 - 一个类可以实现多个接口
interface Alarm {alert();}interface Light {lightOn();lightOff();}class Door {}class SecurityDoor extends Door implements Alarm {alert() {console.log('SecurityDoor alert');}}class Car implements Alarm, Light {alert() {console.log('Car alert');}lightOn() {console.log('Car light on');}lightOff() {console.log('Car light off');}}
6.5.2 接口继承
//接口继承接口interface Alarm {alert();}interface LightableAlarm extends Alarm {lightOn();lightOff();}//接口继承类class Point {x: number;y: number;}interface Point3d extends Point {z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
6.5.3 混合类型
//一个对象可以同时作为函数和对象使用,并带有额外的属性方法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;
7 声明合并
- 函数的合并:使用重载定义多个函数类型
- 接口的合并
- 接口属性的合并:类型必须是唯一的,同名属性类型不一致会报错
- 接口方法:与函数的合并一样
- 类的合并:与接口的合并规则一样
二、类型断言
待更新
