2

TypeScript

什么是 TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript
TypeScript是一个编译到纯JS的有类型定义的JS超集。
  • JavaScript 超集
  • 支持 ECMAScript6 标准,并支持输出 ECMAScript 3/5

JavaScript 与 TypeScript 的区别

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查

img

安装 typescript

npm install -g typescript

tsc app.ts

或者在线 typescript 编译网站:

TypeScript Playground

基础数据类型

  • 布尔类型
let bool: boolean = false;
// js
let bool = false;
  • 数字
let num: number = 6;
// js
let num = 6;
  • 字符串
let str: string = "string";
// js
let str = "string";
  • 数组
let list: number[] = [1, 2, 3];
// js
let list = [1, 2, 3];
  • 元组 Tuple
let x: [string, number] = ["hello", 6];
// js
let x = ["hello", 6];
  • 枚举 enum
enum Direction {
    NORTH
    SOUTH
    EAST
    WEST
}

// js
var Direction;
(function (Direction) {
    Direction[Direction["NORTH"] = 0] = "NORTH";
    Direction[Direction["SOUTH"] = 1] = "SOUTH";
    Direction[Direction["EAST"] = 2] = "EAST";
    Direction[Direction["WEST"] = 3] = "WEST";
})(Direction || (Direction = {}));
  • any
let notSure: any = 4;
  • void

用于标识方法返回值的类型,表示该方法没有返回值。

function hello(): void {
  alert("Hello Runoob");
}
// js
function hello() {
  alert("Hello Runoob");
}
  • null

表示对象值缺失。

let n: null = null;
  • undefined
let u: undefined = undefined;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。

  • never

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

// 返回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。例如:

declare function create(o: object | null): void;

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

TypeScript 断言

类型断言有 2 种形式

  1. “尖括号” 语法
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
  1. as 语法
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

  • 属性判断: in
interface Foo {
  foo: string;
}

interface Bar {
  bar: string;
}

function test(input: Foo | Bar) {
  if ("foo" in input) {
    // 这里 input 的类型「收紧」为 Foo
  } else {
    // 这里 input 的类型「收紧」为 Bar
  }
}
  • 类型判断: typeof
function test(input: string | number) {
  if (typeof input == "string") {
    // 这里 input 的类型「收紧」为 string
  } else {
    // 这里 input 的类型「收紧」为 number
  }
}
  • 实例判断: instanceof
class Foo {}
class Bar {}

function test(input: Foo | Bar) {
  if (input instanceof Foo) {
    // 这里 input 的类型「收紧」为 Foo
  } else {
    // 这里 input 的类型「收紧」为 Bar
  }
}
  • 字面量相等判断 ==, !=, ===, !==
type Foo = "foo" | "bar" | "unknown";

function test(input: Foo) {
  if (input != "unknown") {
    // 这里 input 的类型「收紧」为 'foo' | 'bar'
  } else {
    // 这里 input 的类型「收紧」为 'unknown'
  }
}

高级类型

1. 联合类型(Union Types)

代码库希望传入 number 或 string 类型的参数,可以使用 联合类型做为 padding 的参数:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface Bird {
  fly();
  layEggs();
}

interface Fish {
  swim();
  layEggs();
}

function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
  • 类型保护与区分类型
let pet = getSmallPet();

if ((<Fish>pet).swim) {
  (<Fish>pet).swim();
} else {
  (<Bird>pet).fly();
}

类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for (let id in first) {
    (<any>result)[id] = (<any>first)[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<any>result)[id] = (<any>second)[id];
    }
  }
  return result;
}

class Person {
  constructor(public name: string) {}
}
interface Loggable {
  log(): void;
}
class ConsoleLogger implements Loggable {
  log() {
    // ...
  }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

函数

函数定义

// Named function
function test() {}

// Anonymous function
let test = function () {};

函数类型

function add(x: number, y: number): number {
  return x + y;
}

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

可选参数

编译器会校验传递给一个函数的参数个数必须与函数期望的参数个数一致。

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

function test(a: string, b?: string) {
  if (b) {
    return b;
  } else {
    return a;
  }
}

可选参数必须跟在必须参数后面。

默认参数

function buildName(firstName: string, lastName = "Smith") {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"

剩余参数

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

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

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

省略号也会在带有剩余参数的函数类型定义上使用到:

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

重载

数组

数据解构

let x: number;
let y: number;
let z: number;
let five_array = [0, 1, 2, 3, 4];
[x, y, z] = five_array;

数组展开运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

数组遍历

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

对象

对象解构

let person = {
  name: "tom",
  gender: "male",
};

let { name, gender } = person;

对象展开运算符

let person = {
  name: "tom",
  gender: "male",
  address: "beijing",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

泛型

泛型接口

12.1 泛型接口

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

12.2 泛型类

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;
};

装饰器

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查,有时称为“鸭式辨型法”或“结构性子类型化”。在TypeScript里,接口的作用是为这些类型命名,以及为你的代码或第三方代码定义契约。

interface IQuery {
  page: number;
  findOne(): void;
  findAll(): void;
}

可选属性

interface SquareConfig {
  color?: string;
  width?: number;
}

只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}

readonly 与 const 区别

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly

额外的属性检查

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

或者

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

函数类型

除了用接口描述对象结构外,接口也可以描述函数类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};

可索引类型

interface StringArray {
  [index: number]: string;
}

let myArray:StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

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

使用方式

  • 第一种 传入所有的参数,包含类型参数
let output = identity<string>("myString"); // type of output will be 'string'
  • 第二种 类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定 T 的类型
let output = identity("myString");  // type of output will be 'string'

迭代器与生成器

可迭代性

当一个对象实现了Symbol.iterator属性时,我们认为它是可迭代的。 一些内置的类型如 ArrayMapSetStringInt32ArrayUint32Array等都已经实现了各自的Symbol.iterator。 对象上的 Symbol.iterator函数负责返回供迭代的值。

装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression 这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器工厂

function color(value: string) {
  // 这是一个装饰器工厂
  return function (target) {
    //  这是装饰器
    // do something with "target" and "value"...
  };
}

装饰器组合

  • 书写在同一行上:
@f @g x
  • 书写在多行上:
@f
@g
x

当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合 f 和 g 时,复合的结果(f ∘ g)(x)等同于 f(g(x))。

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如 declare 的类)。

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

方法装饰器

访问器装饰器

class Point {
  private _x: number;
  private _y: number;
  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  @configurable(false)
  get x() {
    return this._x;
  }

  @configurable(false)
  get y() {
    return this._y;
  }
}
function configurable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.configurable = value;
  };
}

属性装饰器

属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare 的类)里。

class Greeter {
  @format("Hello, %s")
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    let formatString = getFormat(this, "greeting");
    return formatString.replace("%s", this.greeting);
  }
}
import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
  return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

参数装饰器

元数据

Mixins

除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 你可能在 Scala 等语言里对 mixins 及其特性已经很熟悉了,但它在 JavaScript 中也是很流行的。

// Disposable Mixin
class Disposable {
  isDisposed: boolean;
  dispose() {
    this.isDisposed = true;
  }
}

// Activatable Mixin
class Activatable {
  isActive: boolean;
  activate() {
    this.isActive = true;
  }
  deactivate() {
    this.isActive = false;
  }
}

class SmartObject implements Disposable, Activatable {
  constructor() {
    setInterval(
      () => console.log(this.isActive + " : " + this.isDisposed),
      500
    );
  }

  interact() {
    this.activate();
  }

  // Disposable
  isDisposed: boolean = false;
  dispose: () => void;
  // Activatable
  isActive: boolean = false;
  activate: () => void;
  deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      derivedCtor.prototype[name] = baseCtor.prototype[name];
    });
  });
}

模块解析

模块解析策略

  • Node
  • Classic

你可以使用 --moduleResolution 标记来指定使用哪种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015 时的默认值为 Classic,其它情况时则为 Node。

  • Classic

相对导入的模块是相对于导入它的文件进行解析的。 因此 /root/src/folder/A.ts 文件里的 import { b } from "./moduleB"会使用下面的查找流程:

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。

example: 有一个对 moduleB 的非相对导入 import { b } from "moduleB",它是在/root/src/folder/A.ts 文件里,会以如下的方式来定位"moduleB":

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts
  • Node
  • 相对路径
  1. 检查/root/src/moduleB.js 文件是否存在。
  2. 检查/root/src/moduleB 目录是否包含一个 package.json 文件,且 package.json 文件指定了一个"main"模块。 在我们的例子里,如果 Node.js 发现文件 /root/src/moduleB/package.json 包含了{ "main": "lib/mainModule.js" },那么 Node.js 会引用/root/src/moduleB/lib/mainModule.js。
  3. 检查/root/src/moduleB 目录是否包含一个 index.js 文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。
  • 非相对路径

Node 会在一个特殊的文件夹 node_modules 里查找你的模块。 node_modules 可能与当前文件在同一级目录下,或者在上层目录里。 Node 会向上级目录遍历,查找每个 node_modules 直到它找到要加载的模块。

TypeScript 是模仿 Node.js 运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript 在 Node 解析逻辑基础上增加了 TypeScript 源文件的扩展名( .ts,.tsx 和.d.ts)。 同时,TypeScript 在 package.json 里使用字段"types"来表示类似"main"的意义 - 编译器会使用它来找到要使用的"main"定义文件。

命名空间

namespace Shape {
    const pi = Math.PI
    // 全局可见
    export function cricle(r: number){
        return pi * r ** 2
    }
}

// js
"use strict";
var Shape;
(function (Shape) {
    const pi = Math.PI;
    // 全局可见
    function cricle(r) {
        return pi * r ** 2;
    }
    Shape.cricle = cricle;
})(Shape || (Shape = {}));

TypeScript编译原理

这里只是简单记录下TypeScript工作流程,毕竟我没很多时间精力阅读它的源码。

  1. TypeScript 源码经过扫描器扫描之后变成一系列 Token;
  2. 解析器解析 token,得到一棵 AST 语法树;
  3. 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
  4. 检查器再次扫描 AST,检查类型,并将错误收集起来;
  5. 发射器根据 AST 生成 JavaScript 代码。

参考资料


看见了
876 声望16 粉丝

前端开发,略懂后台;