按照新的思路再学一遍typescript

1,047 阅读20分钟

什么是ts

ts是一门语言,是js添加了类型的超集,即 A Typed Superset of JavaScript ,经过编译后输出对应js代码以在浏览器或其他运行时上运行。

它的出现主要是为了解决以下几个问题:

  • 添加静态类型系统,能够渐进式的进行静态类型检查发现语言规定不认为是异常的错误
  • 使用stage3阶段和之前的ECMA标准的语法以及其他ts特有的特性
  • 集成编辑器,在编辑时即可捕获错误和代码提示

本文会对以上几点分别进行讲述

静态类型系统

为什么要引入静态类型系统

js在设计之初仅用来作为浏览器中的简单脚本语言,后来随着ecma版本迭代和浏览器为其添加了更多api,使得js的使用越来越复杂,作为一门语言的一些问题也越来越明显。

  • TypeError 直到运行时才能发现,也就是说如果我们测试不充分就无法对其及时修复,如
let foo = "hello!";
foo();
  • 另外还有一些场景虽然和我们希望的运行不一致但是不会报错,比如访问一个对象不存在的属性或者逻辑错误等
let foo = {
  name: "Daniel",
  age: 26,
};

foo.location; // returns undefined

//或
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
  // ...
} else if (value === "b") {
//这个逻辑分支永远不会访问到
}

通过类型系统可以在代码运行之前发现这些错误并得以及时修复。

类型系统中有哪些类型

在类型系统中最重要的是各种类型,包括基本类型和高级类型。

基本类型

包括js语言中的类型和另外添加的类型,这里被分成21种类型来介绍

  • js中原有的7种原始类型
  • 5种ts添加的简单类型
  • 7种复杂类型
  • 泛型和复合类型
7种primitive values
  1. boolean
  2. number
  3. bigint
  4. string
  5. null 见undefined
  6. undefined null和undefined可以一起理解,默认是其他所有类型的子类,即可以赋值给其他类型,当--strictNullChecks设置为true时,null和undefined只能赋值给unknown, any和它们对应的类型(另外undefined还可以赋值给void).
  7. symbol
5种其他简单类型
  1. unknown 表示当代码编写时不确定具体类型,可以接受任何类型,具体使用时可以使用后面提到的type guards缩小类型范围。
  2. any 表示任何类型,可以避免类型检查
  3. void 表示没有任何类型,可以用于无返回函数的返回类型
function warnUser(): void {
  console.log("This is my warning message");
}
  1. never 代表从来不会发生的值的类型,比如一个总是抛出异常的函数返回类型或者一个永远无法执行到头的函数返回值
// Function returning never must not have a reachable end point
function error(message: string): never {
  throw new Error(message);
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
  while (true) {}
}
  1. literal Types字面量类型,以具体的值作为类型,只能赋予对应的值
let a: "u" = "u";
7种较为复杂的类型
  1. enum 枚举,用来定义一组具名常量,保存的内容分数字和字符串两种
    1) 分类
  • 默认保存数字,从0开始,递增,也可以手动修改。同时也可以根据所保存的数据值获得保存的键名,即reverse mapping,如果在reverse mapping过程中多个键保存同一个值,则返回最后一个
enum Direction {
  Up = 1,
  Down,
  Left,
  Right
}

Direction.UP//1
let d:Direction = Direction.UP;

DIrection[1]//UP
let direName: string = DIrection[1];
  • 可以手动初始化为字符串,不支持reverse mapping
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

2)如果包含需要运算得到的的键值对,没有手动赋值的需要放在前面

enum E {
 //Enum member must have initializer.
  B,
  A = getSomeValue()
}
  1. Array 数组类型用两种表达方式,一种是元素类型后面添加[]
let list: number[] = [1, 2, 3];

另一种是使用泛型数组类型Array:

let list: Array<number> = [1, 2, 3];
  1. Tuple Array是保存同一种类型的数组,Tuple可以在确定位置不保存同一种类型
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
  1. object 是所有non-primitive type的父类型
let obj: object = () => {
  return 2;
};
console.log(obj);
  1. interface 接口,通过检查一个值的shape来进行类型检查,只要结构符合某种类型就可以看成是对应类型,可以通过interface关键字或字面量来定义某种类型(具体接口也是一种类型)
//关键字,表示包含一个string类型的属性
interface Test{a:string}
let v:Test={a:'s'}
//等效于
let v:{a:string}={a:'s'}

具体用法:

  • 可选属性,在属性后添加❓,对应属性可选
  • 只读属性,在属性前加readonly关键字,对应属性不能修改
interface SquareConfig {
  color?: string;
  readonly width: number;
}
  • 可索引类型Indexable Types,表示属性名类型为number或string的属性,其中number类型访问前会转化为string类型,多种属性同时存在时需要兼容
interface NumberDictionary {
  [index: string]: number;
  length: number; // ok, length is a number
  name: string; // error, the type of 'name' is not a subtype of the indexer
Property 'name' of type 'string' is not assignable to string index type 'number'.
}
  • 多余属性检查 如果实际的值与其类型相比存在多余类型就会报错
let a: { v: string } = { v: "s", k: 2 };

如果确实有需求,可以通过后面会讲的类型断言

let a: { v: string } = { v: "s", k: 2 } as { v: string };

或者先赋给另一个变量

let b = { v: "s", k: 2 };
let a: { v: string } = b;

或者采用可索引类型

let a: { v: string; [index: string]: number | string } = { v: "s", k: 2 };
  1. functions 函数类型,表示一个函数,通过指定函数的参数类型和返回值类型来确定函数的类型。 可以分别指定参数和返回值类型,这里是函数声明
function add(x: number, y: number): number {
  return x + y;
}

也可以作为一个整体指定函数类型,函数表达式

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

这里也可以通过一个interface来指定,其中参数表示interface的属性名,返回值表示属性值

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

当然也可以同时指定,但没必要。

其他用法:

  • 可选参数 属性后添加❓,需要放在必选属性后面(如果有的话)
  • 默认参数 如果没传递该参数或者传递undefined,则使用对应默认值
function buildName(firstName = "Will", lastName?: string) {
  console.log(firstName, lastName);
}
buildName();
  • rest参数 相当于多个可选参数
function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

console.log(buildName("22"));
  • Overloads 重载,可以根据不同类型的参数进行不同处理,包含重载的签名和具体的实现,注意签名和实现类型的兼容
function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): 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") {
  }
  // Otherwise just let them pick the card
  else if (typeof x == "number") {
  }
}
  • this 这里主要是this在返回的函数和作为参数时的用法,这里暂不讨论。
  1. class 这里既讲class的用法,也讲对应类型以及interface和class之间的其他关系,class本身就是一种type。 1)用法,从es6开始引入了class的用法,使用面向对象的方法进行编程。
    基本的class是这样
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greeter = new Greeter("world");
  • public修饰符,默认所有属性都是公共的
  • private修饰符,私有属性,只能在class内部访问,还可以通过在属性前添加#设为私有
  • protected修饰符,只能在自身和子类内部访问
  • readonly修饰符,只读属性,一定要在初始化或者构造函数中赋值
  • parameter properties参数属性,可以在构造函数参数中添加修饰符,相当于初始化+赋值操作
class Octopus {
  constructor(readonly name: string) {}
}
let dad = new Octopus("Man with the 8 strong legs");
console.log(dad.name);
  • accessors get和set访问器,只设置get相当于readonly
  • static修饰符,静态属性和方法
  • instance属性,没有对应修饰符,默认为实例属性和方法
  • constructor函数,实例化时即调用该函数,属于静态方法
  • extends继承,利用extends继承父class的特性,可以使用super访问父类的属性和方法,在使用this之前需要先调用super()执行父类的构造函数。
  • abtract class,抽象类,不可以直接实例化,用于被继承,其中的抽象方法(前面添加abtract修饰符)只含有签名,即没有‘{}’的函数体,强制子类实现;也可以实现具体的函数,供子类使用。
abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("roaming the earth...");
  }
}

2)类型,一个class分两部分,实例部分和静态部分,具体为:

  • 实例部分,通过实现(implements)一个或多个接口(interface)来实现,只能约束实例部分中的public属性,不能是private或protected
interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}
  • 静态部分,即约束构造函数的类型,而不是实例,比如下面的ClockConstructor
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick(): void;
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
};

3)除了class之间通过extends继承,class对interface进行implements,还包括

  • interface之间继承,可以继承多个(class之间只能继承一个)
interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

interface Square extends Shape, PenStroke {
  sideLength: number;
}
  • interface继承class,可以继承其public、private或protected属性和方法,只能被其本身或者子类实现
class Control {
  private state: any;
}
interface SelectableControl extends Control {
  select(): void;
}
class Button extends Control implements SelectableControl {
  select() {}
}
泛型和复合类型
  1. Generics 泛型,即通用类型,通过传递类型参数实现部分类型通用,比如前面介绍的数组Array,泛型的参数用尖括号'<>'包围,主要用在以下几种类型 1)function,这里可以对照前面讲函数时的写法
    分别指定参数和类型,函数声明
function identity<T>(arg: T): T {
  return arg;
}
identity<number>(2);
//或者直接类型推论
identity(2)

为整体指定类型,函数表达式

let identity: <T>(arg: T) => T = function (arg) {
  return arg;
};
identity(2);

当然也可以同样用一个interface表示

let identity: { <T>(arg: T): T } = function (arg) {
  return arg;
};
identity(2);

2)interface,除了上面interface的内容可以是泛型,其本身也可以是泛型,上面的写法等效于

interface Test<T> {
  (arg: T): T;
}

let identity: Test<number> = function (arg) {
  return arg;
};
identity(2);

泛型interface使用时需要显式传递类型参数
3)class,同样可以类型推论和直接传递类型参数

class GenericNumber<T> {
  zeroValue: T;
  test(a: T): T {
    return a;
  }
}
console.log(new GenericNumber<number>().test(1));
  1. 复合类型,前面介绍的类型都是原子类型, 这里讲不同类型的组合,包括联合类型和交叉类型 1)union type,联合类型,用|分隔不同类型,即其值是这些类型的其中一种,是这其中涉及到确定实际赋值的是什么类型,用后文的Type Guards进行区分。
let A: number | string = 9;

2)intersection type,交叉类型,用&分隔不同类型,即其值要满足所有类型的约束

let A: { a: string } & { b: number } = {
  a: "",
  b: 2,
};

高级类型

高级类型指的是由一种类型根据某种关系生成的另一种类型,分为基本用法和unility

基本用法
  1. Index types,使用keyof操作符生成对应类型的属性名组成的联合类型,如
interface Test {
  a: string;
  b: number;
}
let a: keyof Test = "a";
//相当于
let a:'a'|'b'='a'

注意属性名即index signature只能是number或string,如果属性名是string类型的话,keyof获得是number|string,因为用number访问最终也会转化为用string获取,参考interface部分的相关内容

interface Dictionary<T> {
  [key: string]: T;
}
let keys: keyof Dictionary<number>;
//相当于
let keys: string | number
  1. Mapped types,使用in操作符可以将对应类型的属性统一转换,比如转换成readonly或可选
type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};
  1. Conditional Types,条件类型,如果T属性U类型,则返回X类型,否则返回Y类型
T extends U ? X : Y

这里要注意两个点

  • 如果T是union类型,会依次对各个组成类型进行检验,分别返回
  • 如果返回的是never,则删去对应组成类型
type Diff<T, U> = T extends U ? never : T;
type T1 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
//等效于
type T1= "b" | "d"
  1. Type inference,在条件类型中使用infer标记类型,对应类型可以条件分支中使用,如获取返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;

type T1 = Foo<{ a: string; b: string }>;
//   ^ = type T1 = string
type T2 = Foo<{ a: string; b: number }>;
//   ^ = type T2 = string | number

结合以上会产生更多的操作,这些被封装成了utility

utility

ts提供了一系列全局的类型转换工具,参数为type

  1. Partial 将参数的所有属性设为可选后返回
  2. Readonly 将参数的所有属性设为只读后返回
  3. Record<Keys,Type> 构建一个type,属性名为keys,属性值为Type
  4. Pick<Type, Keys> 在Type中保留属性名extends keys的属性
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};
  1. Omit<Type, Keys> 在Type中去除属性名extends keys的属性
  2. Exclude<Type, ExcludedUnion> 去掉第一个联合类型type中extends 第二个联合类型ExcludedUnion的类型
  3. Extract<Type, Union> 找出第一个联合类型type中extends 第二个联合类型union的类型
  4. NonNullable 排除Type中的null和undefined
  5. Parameters 参数是一个function类型,返回其参数组成的tuple类型
  6. ConstructorParameters 类似Parameters,参数是构造函数
  7. ReturnType 返回值类型
  8. InstanceType 参数是实例函数,即构造函数被实例化后的类型
type T0 = InstanceType<new () => void>;
//    ^ = type T0 = void
  1. Required 和Partial相反,将所有属性设为必选

另外几个关于this的这里暂不讨论

类型的运用

这里包含三部分内容,分别是

  • 处理不同类型之间的关系
  • 对类型的综合运用
  • 项目层级的代码组织

类型之间

  1. Type assertions 类型断言 断言,就是下结论,我们使用断言对某个值手动指定类型,在该值上进行对应类型的操作,用两种方式进行断言,一种是as语法
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;

一种是尖括号语法,由于会和jsx的语法冲突,该语法不能在jsx中使用

let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
  1. Type Inference 类型推论
    即虽然没有显式指出类型,但是可以根据现有条件推算出一个合适的类型.类型推论会发生在变量和成员初始化、设置函数默认参数和返回值时。
    类型推论的方式分为以下几种
  • 根据具体值选择一个最合适的类型,比如
let x = [0, 1, null];
//  ^ = let x: (number | null)[]
  • 根据上下文推论,比如
window.onmousedown = function (mouseEvent) {
  console.log(mouseEvent.button); //<- OK
  console.log(mouseEvent.kangaroo); //<- Error!
};
  1. Type Guards 类型确定 一个变量可能属于多种类型,比如union type,在具体使用时需要缩小类型范围,甚至确定为某种特定类型. 类型确定的方法包括
  • in操作符,通过判断类型中是否有某个属性来缩小范围,比如
function test(param: { a: string } | { b: number }) {
  if ("a" in param) {
  //在这个分支参数是{ a: string }类型
  }
  //在这个分支参数是{ b: number }类型
}
  • typeof,通过判断是或者不是某种类型进行缩小范围,类型的值包括"undefined", "number", "string", "boolean", "bigint", "symbol", "object", or "function"
  • instanceof,通过判断是否是某种构造函数的实例来缩小范围
  • 属性访问,除了in操作符还可以通过点或中括号方式来判断,注意如果使用点,需要先进行类型断言才能访问可能存在的属性,否则会出错。
  • 自定义的type guard,本质就是上述方式的封装,比如
function isNumber(v: number | string): v is number {
  return typeof v === "number";
}
let v = 2;

//使用
if (isNumber(v)) {
  //这里是number
} else {
  //这里是string
}

其中v is number,即parameterName is Type,parameterName是当前参数的类型之一,函数返回布尔值,来表示是否为某一类型

  1. Type Compatibility 类型兼容 即把一种类型的值赋值给另一种类型的变量是否接受。ts的类型兼容是根据结构判断的。
    类型兼容包括以下几种
  • 基本规则,至少包含相同的属性,这里的细节和interface一节提到的多余类型检查一致。
  • 对比两个函数,会根据基本规则分别检查参数和返回值
  • 对比两个class,只检查它们的实例属性,其中public、private和protected属性分别对应

综合运用

  1. Type Aliases 类型别名
    前面介绍了很多类型,可以为那些类型设置一个易用的别名来表示,比如
type Test={
a:string
}
type a=string

除了一个(即interface可以通过声明多个进行扩展,这里会在下文Declaration Merging中提到),type aliase可以使用interface实现其他所有的功能。

  1. Namespaces 命名空间 命名空间就是在module出现之前用于组织全局作用域的代码,用了module就不要用namespace,这里作为了解。 命名空间的内容通过其变量名来访问,内部变量只能在内部使用,如果要对外部暴露需要使用export进行操作。
namespace A {
  let a = 1;
  export const b = {
    k: "2",
  };
}
console.log(A.b);

如果将namespace放在多个文件,需要对文件进行引入,以及保证最终执行顺序,这里可以通过三斜线指令实现,比如

/// <reference path="Validation.ts" />
  1. Declaration Merging 声明合并
    声明合并是多个同名的声明可以按照一定规则合并成一个声明.
    在讲声明之前先了解一下相关概念,这有助于理解声明合并时合并的是啥东西。
    在ts中一次声明至少创建三种实体中的一种:namespace、type和value,而声明方式也有很多,每种声明创建的实体如下 |声明类型|namespace|type|value |-|-|-|-| |Namespace|√| |√ |Class| |√|√ |Enum| |√|√ |Interface| |√ |Type Alias| |√ |Enum| | |√ |Variable| | |√

具体合并类别为:

  • interface合并,这分为几种情况
    • 对于非函数属性,则同名属性类型应一致否则报错
    • 对于函数属性,会视为重载,且后面的接口中的属性优先级高,但是当参数类型是字符串字面量类型时,会冒泡到最高优先级
  • namespace合并,导出的变量会互相合并,合并规则和接口合并类似
  • Namespaces和Classes, Functions, and Enums合并,合并时namespace需要紧随要合并的类型后面,结果就是对应类型有了namespace暴露出来的内容

代码组织

我们知道了有哪些类型和怎么区分和使用不同类型,在大型项目中代码(包括类型和具体功能代码)的组织方式,还需要进一步处理。这一节会介绍module、类型声明文件、配置文件以及模块和类型声明引入方式。

Module

模块在各自的作用域执行,除非显式导出,模块之间不可见。在模块里引入另一个模块需要使用module loader(比如node.js中的commonjs和es6中的es module),loader会在当前模块执行前加载并执行依赖的模块。 在ts中一个含有顶级import或export的文件可以看成是一个模块,否则全局作用域可见。。
另外,ts中添加了特有的导入导出语法:export = and import = require()

Declaration Files

在我们使用的项目中,我们所使用的一些api并没有类型声明,我们需要手动为它们添加对应的声明,即编写声明文件,即以.d.ts为后缀的文件(当然也可以在.ts文件内声明),各种声明以declare开头,declare代表当前上下文已经有这个value,我们只是声明这个值的类型

  1. Declaration Reference 类型声明就是声明各种类型,现在我们回顾以下对于各种类型怎么声明。
  • 声明带有属性的全局对象,当然属性也可以是类型,用namespace,这种namespace称作ambient namespaces
let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);
let count = myLib.numberOfGreetings;
let v: myLib.Test = {
  a: "",
};

declare namespace myLib {
  function makeGreeting(s: string): string;
  let numberOfGreetings: number;
  interface Test {
    a: string;
  }
}
  • 声明重载函数
let x: Widget = getWidget(43);
let arr: Widget[] = getWidget("all of them");
declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];
  • 声明type或interface,两者写法相似
greet({
  greeting: "hello world",
  duration: 4000
});

用type声明

declare type GreetingSettings = {
  greeting: string;
  duration?: number;
  color?: string;
};

declare function greet(setting: GreetingSettings): void;

用interface声明

interface GreetingSettings {
  greeting: string;
  duration?: number;
  color?: string;
}
declare function greet(setting: GreetingSettings): void;
  • 声明class,这里声明的class可以包含构造函数
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();

class SpecialGreeter extends Greeter {
  constructor() {
    super("Very special greetings");
  }
}
declare class Greeter {
  constructor(greeting: string);

  greeting: string;
  showGreeting(): void;
}
  • 声明全局变量
console.log(a)
declare var a: number;
  • 声明全局函数
greet("hello, world");
declare function greet(greeting: string): void;
  1. 为library提供声明文件 我们已经知道具体每一种类型怎么声明了,当我们为某个library编写声明文件时,还需要确定其结构。
    不过我们在日常项目中很少需要编写第三方library的具体声明文件,很多library本身自带声明文件,没有的我们只需声明module或者第三方编写的声明文件即可。
  • 自带声明文件,下载这一类library时,会自动下载声明文件,比如
npm i moment
  • 声明模块,即ambient modules
import a from "aaaa";
console.log(a.console());
declare module "aaaa" {
  let console: () => void;
}
//或简短声明为any
declare module "aaaa";
//或Wildcard module
declare module "*aa";
  • 在npm上的每个library的声明文件名字都是@/type/[library name],可以直接下载,比如
npm install --save @types/lodash
  1. module augmentation和global augmentation
  • module augmentation,模块扩展,虽然模块不支持合并,但可以引入现存的模块然后扩展,比如
// observable.ts
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}

// map.ts
import { Observable } from "./observable";
declare module "./observable" {
  interface Observable<T> {
    map<U>(f: (x: T) => U): Observable<U>;
  }
}
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map((x) => x.toFixed());
  • Global augmentation,这里指的在module内扩展全局作用域,如果在非模块内直接声明即可。
import a from "aaaa";
console.log(a.console());
declare global {
  let sss: string;
}
console.log(sss);

配置文件tsconfig.json

配置文件在项目根目录,用来指定整个项目的需要直接编译的文件(这些文件可能还会使其他文件加入编译)和编译选项。
编译选项很多,可以根据tsconfig.json Schema了解一下整体结构,由schema文件可见,整个配置文件根级选项有9个,分别如下

 "allOf": [
    { "$ref": "#/definitions/compilerOptionsDefinition" },//编译选项
    { "$ref": "#/definitions/compileOnSaveDefinition" },//保存时编译自动编译
    { "$ref": "#/definitions/typeAcquisitionDefinition" },//自动引入库类型定义文件
    { "$ref": "#/definitions/extendsDefinition" },//继承其他配置文件
    { "$ref": "#/definitions/tsNodeDefinition" },//ts-node相关
    {
      "anyOf": [
        { "$ref": "#/definitions/filesDefinition" },//需要编译的文件列表
        { "$ref": "#/definitions/excludeDefinition" },//编译需要排除的文件或文件夹
        { "$ref": "#/definitions/includeDefinition" },//编译包含的文件或文件夹
        { "$ref": "#/definitions/referencesDefinition" }
      ]
    }
  ]

具体使用参考TSConfig Reference,这里提几个选项,方便理解下一部分内容。 指定需要编译的文件包括以下选项

  • files 需要包含的文件列表组成的数组,用在文件少的时候
{
  "compilerOptions": {},
  "files": [
    "core.ts",
    "sys.ts"
  ]
}
  • include 用 glob patterns指定文件
{
  "include": ["src/**/*", "tests/**/*"]
}
  • exclude 排除include选项指定的文件中的部分文件,其他方式引入的不受影响,比如importtypes选项、files选项和 /// <reference>指令。

模块和类型声明引入

根据前面的知识,我们有了零散的类型声明和具体实现,包括全局作用域和模块作用域的,并且可以通过配置文件将各种文件内的代码通过编译到同一个文件进行使用,但是现在存在两个问题需要解决:

  • 模块作用域的文件怎么解析供项目使用?
  • 不同文件的全局作用域代码排列顺序怎么处理? 下面的内容会给出答案
Module Resolution

模块解析用于找出import引入的到底是什么东西,只有知道了确切内容才可以进一步处理。当遇到import语句时,以import { a } from "moduleA";为例,默认解析过程如下:

  1. 编译器根据指定的模块解析策略:classic或者node,对模块文件进行定位
  2. 如果没找到和且模块名是非相对模块,编译器就会查找前面讲的ambient module声明
  3. 如果还没找到就报错TS2307: Cannot find module 'moduleA'.

相对和非相对模块

相对模块的路径以 /, ./ or ../开始,默认直接计算路径,rootDirs选项会影响其结果;
否则就是非相对路径,其可以基于baseUrl进行解析,也可以通过Path mapping,可以用来解析 ambient module声明和外部依赖, 具体到某个非相对路径的解析,会先按照baseUrl等的配置进行解析,然后再根据默认解析过程进行解析,这一点没查到相关文档,但可以根据使用tsc --traceResolution进行验证。

这里介绍几个前面提到的影响路径解析的配置

  1. rootDirs 将多个目录合并为一个虚拟目录,相对路径解析到其中一个时会在多个目录中依次查找,就像在同一个目录下一样
  2. baseUrl 用来告诉编译器到哪里去找module,所有非相对目录都是假定相对于该值的路径进行解析
  3. paths 指定Path mapping即路径映射,这里的路径会相对于baseUrl进行解析,比如
{
  "compilerOptions": {
    "baseUrl": ".",// This must be specified if "paths" is.
    "paths": {
      "*": ["*", "generated/*"]// This mapping is relative to "baseUrl"
    }
  }
}

在这个配置里的路径映射分别表示

  • "*": => /
  • "generated/*": => /generated/

模块解析策略

包括classic和node,通过moduleResolution指定

  1. classic,用于兼容以前的策略,解析文件后缀和优先级为.ts和.d.ts。
    对于相对路径会直接解析相对于当前文件路径的文件,比如在/root/src/folder/A.ts文件执行import { b } from "./moduleB" ,则解析顺序为
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts

对于非相对路径,会从当前文件所在的目录开始向上遍历到根目录寻找,比如在/root/src/folder/A.ts文件执行import { b } from "moduleB",则解析顺序是

/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
  1. node,模仿node.js的策略。
    对于相对路径,首先会按顺序请求相对路径下对应name的.ts, .tsx, and .d.ts,然后找对应name的目录下的package.json的types字段指定的文件,然后是对应目录下的name为index,三个后缀的文件,比如在/root/src/moduleA.ts执行import { b } from "./moduleB" ,解析顺序是
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (if it specifies a "types" property)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts

对于非相对路径,和处理相对路径类似,会从当前文件所在目录开始到根目录依次查找node_modules目录下的对应文件,每级目录查找顺序和相对查找类似,除了在package.json后多了一步查找对应类型声明,比如在/root/src/moduleA.ts执行import { b } from "moduleB",首先在/root/src/node_modules/目录下查找,即

/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (if it specifies a "types" property)
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts

然后依次向上级目录重复以上过程。

Triple-Slash Directives

三斜线指令就是包含XML单标签的单行注释,在每个文件最上面用作编译指令,这些指令不常用,这里只介绍一个较为常见的。
/// <reference path="..." />
用作指定类型文件的依赖,可以用来将那些没有被配置文件指定的文件添加进渲染过程。
当编译器对配置中指定的文件处理前,会对它们的三斜线指定进行预处理,将那些指定的类型文件按照顺序进行处理,以保证引入顺序。

新特性

相对于类型系统,这一块功能算是锦上添花,就像前面提到的,这里的新特性指的是一些ts特有的功能和esma标准中stage3和之前的所有语法特性,经过编译器编译后可以转换为指定target的语法。

其中ts特有的特性,比如Enum、class的新语法等。

至于ecma标准的最新语法,可以使用babel(想了解babel更多内容参考这篇),后者给了更多更灵活的使用方式,且提供了对浏览器环境的polyfill。两者的结合参考Using Babel with TypeScript.

集成编辑器

通过对编辑器的集成(包括 Visual Studio, Visual Studio Code, Nova, Atom, Sublime Text, Emacs, Vim, WebStorm and Eclipse.),利用开发时的错误捕获和代码提示,大大提高了开发效率

结语

以上就是这次分享的全部内容,将ts官方文档重新整理了顺序并加入了自己的理解。
对于其中的排版问题和错误理解,后续会持续修正。

参考