Typescript 文档读过几次, 每次读了就忘, 尝试做下笔记, 把书读薄, 看看有没有用。

基本类型

原始类型

boolean, number, string, object, null, undefined, symbol

数组

let list: number[] = [1, 2, 3];        // c 风格
let list: Array<number> = [1, 2, 3];   // 泛型风格

元组 Tuple

let x: [string, number]
x = ['hello', 10] // initialize
console.log(x[0], x[1]) // access

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[6] = true; // Error, 布尔不是(string | number)类型

这种越界访问的意义到底何在? 把元组似乎变成了一个联合类型的数组?

枚举

// 索引默认从0开始, 加 1 自增
enum Color {
  Red,        // 0
  Green,      // 1
  Blue        // 2
}

// 指定值
enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}

// 数字枚举支持从数字反查
console.log(Color[4]) // output Blue

// 字符串枚举, 不支持反查
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

// 动态枚举值
enum E {
  A = getSomeValue(), // A 动态生成
  B,                  // error! B 一定要显示的赋值
}

常量字面量的枚举类型的成员也是类型

enum ShapeKind {
    Circle,
    Square,
}

let kind: ShapeKind.Circle = ShapeKind.Circle
kind = ShapeKind.Square // 类型不兼容
kind = 100 // 但是赋值任意的数字给 kind,均不报错. 😂 为啥呢?

也就是说 ShapeKind.Circle 即是值,又是类型。

运行时的枚举

ts 转为 js 后,(非 const)枚举显然是以对象的形式存在的。所以 ts 让枚举可以兼容对象:

enum E {
  X, Y, Z
}

function f(obj: { X: number }) {
  return obj.X;
}

f(E)

const 枚举

这里的 const 含义并不代表枚举的值不能修改(实际上无论有没有这个 const,枚举的值都不能修改)。这里的 const 是给 ts 编译器一个信息: 这个枚举里都是常量,不要把这个枚举存在对象里了,把他们的直接内敛到代码里去:

const enum E {
  X, Y = X * 2, Z
}

function f(e: E) {
  return e;
}

f(E.X)
f(E.Y)
f(E.Z)

转移成 js 后为:

function f(e) {
    return e;
}
f(0 /* X */);
f(0 /* Y */);
f(1 /* Z */);

所以这个是一个性能优化的方法。

外部枚举

文档的字认识,但是不明白啥意思.

any

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为Object有相似的作用,就像它在其它语言中那样。 但是Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

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'.

void

一般用于表示函数没有返回值:

function warnUser(): void {
    alert("This is my warning message");
}

null 和 undefined

这 2 种类型分别只有1个取值,就是它们自己。且默认情况下 null 和 undefined 是所有类型的子类型, 所以它们可以被赋值给任意类型。

never

表示永不存在的值,一般用于表示函数不会到达终点(会抛出异常,或者死循环)

  // 返回never的函数必须存在无法达到的终点
  function error(message: string): never {
      throw new Error(message);
  }

  // 返回never的函数必须存在无法达到的终点
  function infiniteLoop(): never {
      while (true) {
      }
  }

类型断言(强制类型转换)

2 种形式:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

as 看起来更现代一点, 😂.

函数类型

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

let myAdd: (baseValue: number, increment: number) => number = Test  // 1
let myAdd1: { (x: number, y: number): number } = Test               // 2

写法 1 比较常用, 写法 2 对应于 通过 interface 定义函数,即 接口 一节里介绍的函数类型

构造函数类型

构造函数类型,亦称类类型

class Test {
    constructor(x: string) {}
}

let ctor: typeof Test = Test                   // 1
let myAdd1: { new(x: string): Test } = Test    // 2

写法 1 比较常用, 也简洁。写法 2 常用于构造构造函数泛型

接口

“鸭式辨型法”, “结构性子类型化”, 一般情况下只要结构吻合, 则认为类型兼容

// 定义接口
interface SquareConfig {
  width: number;                                 // 必须的类型
  color?: string;                                // 可选类型
  readonly x: number;                            // 只读类型
  [index: number | string]: any;                 // 可索引类型
  (source: string, subString: string): boolean;  // 函数类型
  setTime(d: Date)                               // 函数方法
}

其中函数类型用于表示该类型是一个函数. 函数方法用于定义类的成员函数。

函数方法是很容易理解的,和一般语言是类似的。但是函数类型的这种声明看起来很让人困惑, 它实际上等价于用 type 声明了一个函数类型的别名:

interface Type {
    (x: number, y: number): void // Type 是一个函数,且符合这个声明
}

等价于:

type Type = (x: number, y: number) => void

上面2中声明,都能让下面的这个赋值合法:

let z: Type = function (x: number, y: number) { }

问题是 type 定义明明清楚明白,干嘛要搞个函数方法呢?这可能也是无奈之举了,js 里的变量很有可能既是函数,又是包含属性的对象,而且这在 js 开发中是大量使用的。例子:

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { return '' };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

额外的属性检查

对象字面量会被特殊对待而且会经过额外属性检查

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

function createSquare(config: SquareConfig): void {
}

createSquare({ color: "red", width: 100, property: 1 });
      // 报错, 类型不兼容, property 是一个接口里没有声明的额外属性

let variable = { color: "red", width: 100, property: 1 }
createSquare(variable) // 赋值给一个变量,类型兼容了

这个特性被搞出了,是专门针对 对象字面量 的么,来搞我们程序员的么?心里 mmp

类静态部分与实例部分的区别

定义一个类包含2部分: 即静态部分和实例部分, 构造函数和静态方法都属于类的静态部分,而当去实现一个接口的时候你是在约束你的实例部分,所以下面这样的写法是不对的:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

上面这个官方给的例子里 interface 的这个声明,即不符合函数类型的定义格式,也不符合函数方法的定义格式,从其文档上下文的推测来看,这是一种特殊的用法,应该属于函数类型的定义格式,表明这个 interface 声明的是一个构造构造函数。

interface ICoordinate {
    x: string;
    y: string;
}

class Coordinate implements ICoordinate {
    constructor(public x: string, public y: string) {
    }
}

这是标准的实现接口的方法, 如果用 es5 来写这个构造函数,则为:

function Coordinate(x, y) {
    this.x = x;
    this.y = y;
}

Coordinate 是类名,但本质上还是是个函数.

// 定义一个描述构造函数的 interface
interface CoordinateConstructor {
    new (x: string, y: string): Coordinate
}

// 这个赋值合法
let ctor: CoordinateConstructor = Coordinate

继承于类的接口

接口可以通过继承类来获得,(😂 这特性没在其他语言里见过),而且这里还有个特别的地方,当继承的类有 private 或者 protected 属性时,ts 的 鸭式辨型法 进化成了和 C#/java 等语言的名义类型 (nominal typing) 了。

class Control {
  protected state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() { }
}

class MyImage implements SelectableControl { // 错误
  protected state: any;
  select() { }
}

上例中,MyImage 虽然实现了 SelectableControl 接口的所有方法 (select) 并声明了所有的属性 (state),但是 state 属性在 Control 类里是 protected 的, 这时 ts 的 typing 系统就会变得严格起来,它要求 state 一定要是 Control 里声明的那个, 也就是说 MyImage 一定要通过继承 Control 才能拥有这个属性。

这特么,也是有意来搞人的把。😂

class Greeter {
    static sProperty: Date = new Date();                   // 共有静态变量
    private static sProperty1: Date;                       // 私有静态变量
    greeting: string;                                      // 属性声明
    private readonly property1: number;                    // 只读属性,在构造函数里初始化
    readonly numberOfLegs: number = 8;                     // 直接初始化的只读属性
    constructor(message: string, public date: string) {    // 构造函数,date 为参数属性
        this.greeting = message;
        this.property1 = 9;
    }
    greet() {
        Greeter.sProperty = new Date('2018-9-5');
        return `Hello ${this.greeting} on ${this.date}`;
    }
    static sMethod() {
        console.log(Greeter.sProperty);
    }
}


Greeter.sMethod()
let greeter = new Greeter("world", '2018-9-6');
console.log(greeter.greet()); // hello world on 2018-9-6
Greeter.sMethod()

继承

class Animal {
    constructor(public name: string) { }
    move(distanceInMeters: number = 0) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) {
      super(name);                    // 调用父类的构造函数
    }
    move(distanceInMeters = 5) {
      console.log("Slithering...");
      super.move(distanceInMeters);   // 通过 super 调用父类方法
    }
}

存取器

记一下语法:

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

抽象类

和其他语言类似,记一下语法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

类的构造函数类型

类本身的类型(类的构造函数类型)是啥呢: typeof [ClassName]

class Greeter {
    static standardGreeting = "Hello, there";
}

let Construct: typeof Greeter = Greeter;

函数

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

可选参数和默认参数

默认参数可以认为是一种特别的可选参数

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

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

他们共享同一个类型,即:

(firstName: string, lastName?: string) => string

剩余参数

和 es6 区别不大, 注意类型的格式

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

函数重载

这个所谓的函数重载和一般语言里的函数重载在意义上似乎完全不同。这里的函数重载是为了解决同一个函数传入不同类型的参数返回不同类型的值的问题。语法也很清奇:

function getNumber(n: number): number;  // 重载1
function getNumber(n: string): string;  // 重载2
function getNumber(n: any): any {       // 兼容重载1 和 重载2 的一个声明, 本身不算重载
    return n;
}

let a: number = getNumber(1)       // a 的类型是 number
let b: string = getNumber('1')     // b 的类型是 string
let c: boolean = getNumber(true)   // getNumber 不能接受除了 number 和 string 意外的参数

这种重载,也可以用在类里成员函数和静态函数都可以 这就叫重载,你气不气?

常规意义上的重载是不允许的, 需要用泛型去处理下面的这种情况:

function getNumber(n: number): number {}

function getNumber(n: string): string {} // error: duplicate function implement

泛型

和其他语言类似:

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

或者

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

<T>(arg: T) => T{<T> (arg: T): T} 等价

用 interface 来定义这个函数类型:

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

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

let a: GenericIdentityFn = identity
identity<number>(1)     // 明确泛型为 number
identity<string>(1)     // 类型不匹配
identity('1')           // 类型推断

将泛型语法函数转为非泛型函数:

let numberIdentity: GenericIdentityFn<number> = identity;
let stringIdentity: GenericIdentityFn<string> = identity;

泛型类

与一般语言,并无太多区别, 如果你熟悉 java 或者 C# 的泛型, ts 的就是个简化版。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();

泛型约束

看例子秒懂,对吧。

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

在泛型约束中使用泛型参数

function getProperty<T>(obj: T, key: keyof T) {
    return obj[key];
}

let obj = {a: 1, b: 2};

getProperty(obj, 'a') // OK, 'a' 可以被赋予联合类型 'a' | 'b'
getProperty(obj, 'c') // error, 'c' 不能被赋予联合类型 'a' | 'b'

keyof [泛型约束] 代表着 T 类型对象所有 key 的联合类型, 比如上例中的这次调用,第二个参数的类型是 'a' | 'b', 其完整的函数定义为:

(obj: { a: number, b: number }, key: "a" | "b") => number

在泛型里使用构造函数类型

构造函数类型,亦称类类型

使用的是 构造函数类型 声明的写法2

function create<T>(c: {new(): T}): T {
    return new c();
}

类型推导

最佳通用类型原则: 看当前的语句,找出所有候选类型里最佳的那个类型.

class Animal { }
class Rhino extends Animal { }
class Elephant extends Animal { }
class Snake extends Animal { }

let zoo = [new Rhino(), new Elephant(), new Snake()];

上例中 zoo 里元素的类型有: Rhino, Elephant, Snake. 虽然我们人知道他们都继承于 Animal, 但是 typescript 并不会去做这样的推导,他只会看他已知的这几个类型,发现并不能找到一个类型能兼容其他的, 所以 zoo 就会被推导成兼容这3种类型的联合类型的数组: (Rhino | Elephant | Snake)[].

如果我们改一下上例,

let zoo = [new Rhino(), new Elephant(), new Snake(), new Animal];

这是 zoo 就会被推导成 Animal[] 了。

类型兼容性

对象和类的兼容性

一般来说是“鸭式辨型法”, 但是也有例外:对象字面量的额外属性检查, 继承于有非 public 成员的类的接口

类的兼容性方面还有2个特别的之处:

  1. 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
  class Animal {
      static x: string;
      feet: number;
      constructor(name: string, numFeet: number) { }
      static method() { }
  }

  class Size {
      static y: string;
      feet: number;
      constructor(numFeet: number) { }
  }

  let a: Animal;
  let s: Size;

  a = s;  //OK
  s = a;  //OK
  1. 类的非 public 成员会导致 ts 的类型系统从 “鸭式辨型法” 升级为 nominal typing.

如下例:

class Animal {
  private feet: number;
}

class Size {
  feet: number;
}

let a: Animal;
let s: Size;

a = s;
s = a;

函数

参数少了没关系, 返回值多了没关系。仔细想一下, 也很好理解。

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property

函数参数双向协变 和 可选参数及剩余参数, 函数重载

函数参数双向协变: 看例子似乎就是强制类型装换,这样做很危险。 可选参数及剩余参数: 完全未明其义。 函数重载: 没有什么特别之处

枚举

enum AnimalType { Dog, Elephant, Fish }

enum Color { Red, Blue, Green };

let a: number = AnimalType.Dog
let b: AnimalType = 3
let c: Color = AnimalType.Dog // 不兼容

泛型类型

泛型类型比较时, 会把所有泛型参数当成 any 去比较

let identity = function<T>(x: T): T { }
let reverse = function<U>(y: U): U { }
identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any

高级类型

交叉类型

表示包含多种类型所有属性的类型,多用于对象的合并:

class Animal {
    constructor(public name: string) { }
}

class Fly {
    constructor(public fly: string) { }
}

let bird: Animal & Fly = Object.assign(new Animal('bird'), new Fly('can fly'))

上例中 bird 包含 Animal 和 Fly 类的所有属性, 其类型可表示为 Animal & Fly。

联合类型

联合类型表示一个或者的关系,比如 string | number, 表示这个变量可以是 string, 也可以是 number.

function print(text: string | number) {
  console.log(text);
}

print(1);       // OK
print('2');     // OK
print(false);   // error

类型保护

class Bird {
  fly() {}
  layEggs() {}
}

class Fish {
  swim() {}
  layEggs() {}
}

function getPet(): Fish | Bird {
  return Math.random() > 0.5 ? new Fish() : new Bird()
}

let pet = getPet() // getPet()的返回值类型是`Bird | Fish`
pet.layEggs() // 允许, Bird 类 和 Fish 类都有此方法
pet.swim() // 报错, pet 的类型不能确定为 Fish

要解决这个问题:

  1. 可以通过类型强转(即所谓的断言)后, 然后检查函数是否存在来判断类型
     (pet as Fish).fly && (pet as Fish).fly()
    

    ts 对这种方案处理的不太好, 所以需要2次转. 看来也是不建议使用。

  2. 使用 instanceof 关键字
     if (pet instanceof Fish) {
       pet.swim()
     }
    

    ts 对这个方式支持不错

  3. ts 还提供了一个清奇的方案,即本节的主角类型保护
     function isFish(pet: Fish | Bird): pet is Fish {
       return (<Fish>pet).swim !== undefined;
     }
    
     isFish(pet) && pet.swim()
    

    WTF!? 好吧,ts 提供了这么一个特殊的返回值后, 被这种返回值的函数调用过后,参数的类型就确定下来了。先知道一下吧,返回值的类型格式为 parameterName is Type

    真的很惊喜和意外,也很受惊。😂

typeof 类型保护

使用 typeof 时, 当期作用于 number, string, boolean 以及 symbol 时, ts 对其做了类型保护, 即类型会变得明确。

function getVariable() {
  let r = Math.random()

  if (r < 0.25) {
    return 1
  } else if (r >= 0.25 || r < 0.5) {
    return 'i am string'
  } else if (r >= 0.5 || r < 0.75) {
    return true
  } else () {
    return Symbol()
  }
}

let v = getVariable() // type: number | string | boolean | symbol

if (typeof v  === 'number') {
  // v is number
} else if (typeof v === 'string') {
  // v is string
} else if (typeof v === 'boolean') {
  // v is boolean
} else (typeof v === 'symbol') {
  // v is symbol
}

用 ! 去除 null 和 undefined 类型

这是开启 strictNullChecks 选项后的一个技巧。一般来说想要去除 null 或者 undefined 类型,用简单的判断就够了, 比如:

function test(name: string | null) {
    return name === null
        ? ''
        : name.charAt(0)
}

一些特殊情况下,你可能需要强制去去除 null 和 undefined 类型,比如:

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

类型别名

  type NameResolver = () => string;
  type Container<T> = { value: T };                 // 和泛型一起使用
  type LinkedList<T> = T & { next: LinkedList<T> }; // 和交叉类型, 泛型一起使用
  type Yikes = Array<Yikes>; // error               // 不可以出现在右侧

类型别名不创造一个新的类型,它仅仅是别名。所以你无法 extends 或者 implements 它。

何时用类型别名?何时用接口?

接口会创建实实在在的类型。如果你需要表示对象,一般建议使用interface。别名更适合用于表示联合类型, 交叉类型等书写起来冗长的类型。

// good
interface CoordinateA { x: number; y: number}
function printA(c: CoordinateA) {
    console.log(c.x, c.y)
}
let a: CoordinateA = { x: 1, y: 2 }
printA(a)

// bad
type CoordinateB = { x: number; y: number}
function printB(c: CoordinateB) {
    console.log(c.x, c.y)
}
let b: CoordinateB = {x: 3, y: 4}
printB(b)

// 合适
type MyType = string | number | boolean | symbol
function demoFunction(data: MyType): MyType { return MyType } // 简洁

字符串和数字字面量类型

字符串字面量和数字字面量也是类型。

function print(val: 'cat') {}
print('cat') // ok
print('dog') // error

function print1(val: 1) {}
print1(1) // ok
print1(2) // error

这个例子有点神经病了,不过只是展示。这种类型更多的是像下例这样使用:

// 工厂方法
function creator(type: 'dog' | 'cat' | 'horse' ): Animal {
  switch(type) {
    case: 'dog':
      return new Dog();
    case: 'cat':
      return new Cat();
    case: 'horse':
      return new Horse();
  }
}

let animal = creator('dog')     // OK
let animal1 = creator('snake')  // error

可辨识联合

所有类型的共有属性,可以在他们的联合类型上访问:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle; // 3个类型共有 kind 类型

function area(s: Shape) {
    switch (s.kind) {                     // Shape 类型就有 kind 类型
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

完整性检查

属于开发技巧, 详见: https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/Advanced%20Types.html

多态的 this 类型

this 即是变量,又是类型,其具有多态性。this 返回的是对象本身,这种多态性很好理解。

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
}

let v = new ScientificCalculator(2)
  .multiply(5)
  .sin()              // 体现多态
  .add(1)
  .currentValue();

索引类型

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};

let strings: string[] = pluck(person, ['name']); // ok, string[]

例子有点复杂, 这几个特别的操作符:

这 2 点都还好理解, 我个人一直觉得奇怪的就是 K extends keyof T 这个表达式了。后来想了一下,我们可以分开这样来理解:

K extends keyof T 这种写法相当于给 keyof T 搞了一个别名 K, 且同时能够强调参数 name 的这个 keyof T 类型和返回值里这个 keyof T 类型是一致的,有助于类型推断。这样说有点抽象,我们把上例换个写法:

interface Person {
    name: string;
    age: number;
}

function pluck<T>(o: T, names: Array<keyof T>): T[keyof T][] {
  return names.map(n => o[n]);
}

let person: Person = {
    name: 'Jarid',
    age: 35
};

let strings: Array<string | number> = pluck(person, ['name']); // 类型无法推断

注意最后一行, strings 无法被推断成 strings[] 类型了。 这就是 function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] 这个声明厉害的地方了,表达了多重的信息,而且只要一行代码。

映射类型

把 for…in 语法用用到了类型声明, 一般用于改变属性的一些特性:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

等价于

type Flags = {
    option1: boolean;
    option2: boolean;
}

映射类型来创造一些类型:

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

interface Name {
  firstName: string;
  lastName: string;
}

type NullableName = Nullable<Name>
type PartialName = Partial<Name>

那么相当于:

type NullableName = {
  firstName: string | null;
  lastName: string | null;
} // 当然开启 strictNullChecks 选项后,这个声明才有意义

type PartialName = {
  firstName?: string;
  lastName?: string;
}

一下这4个映射类型都属于 ts 标准库, 可以直接被使用。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

type Partial<T> = {
    [P in keyof T]?: T[P];
}

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

type Record<K extends string, T> = {
    [P in K]: T;
}

唯一需要说明的是字符串字面量类型和他们的联合都属于 K extends string 类型。

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

等价于

type ThreeStringProps = {
  prop1: string;
  prop2: string;
  prop3: string;
}

一个复杂的例子:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}

function proxify<T>(o: T): Proxify<T> {
    let result = {} as Proxify<T>

    for (let prop in o) {
        Object.defineProperty(result, `_${prop}`, {
            writable: true,
            enumerable: false,
            value: o[prop]
        });
        result[prop] = {
            get() { return result[`_${prop}`] },
            set(value) { result[`_${prop}`] = value }
        }
    }

    return result
}

let proxyProps = proxify({
    a: 1,
    b: 2
});

alert(proxyProps.a.get())
proxyProps.a.set(9)
alert(proxyProps.a.get())

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

console.log(originalProps)

看着这些泛型,头晕了么。😂

模块

和 es6 的模块系统区别不大。export and import 你懂的。

导出:

// 导出变量
export let a = 1;

// 导出类
export class Test { }

// 导出函数
export function Test () {}

// 导出默认函数, 函数名可以省略
export default function () {}

// 导出默认类
export default class {}

// 导出默认变量,类名可以省略
export default {
  a: 1,
  b: 2
}

// 导出另一个名字
export { Test as Test1, a as a1 }

// 重新导出
export { A, B1 as B } from "./ZipCodeValidator";

// 重新导出所有
export * from "./StringValidator";

导入:

// 导入默认和个别的导出变量
import obj, { a, Test } from 'export.ts';

// 导入所有导出变量
import * as validator from "./ZipCodeValidator";

// 全局导入
import "./my-module.js";
declare let $: JQuery;
export default $;

生成模块代码

根据编译选项的不同,ts 代码可以被编译成各种不同的模块,详见我之前写的一篇文章

有一点值得注意,ts 编译出来的所谓的 UMD 模块, 并不支持全局变量,不能再浏览器中直接使用。

模块的动态加载

使用

declare function require(moduleName: string): any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
    let validator = new ZipCodeValidator();
    if (validator.isAcceptable("...")) { /* ... */ }
}

使用其它的JavaScript库

需要这个库的定义文件.d.ts, 类似于 C++ 里的头文件。

例子:

// node.d.ts
declare module "url" {
    export interface Url {
        protocol?: string;
        hostname?: string;
        pathname?: string;
    }

    export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}

declare module "path" {
    export function normalize(p: string): string;
    export function join(...paths: any[]): string;
    export let sep: string;
}

通过 /// <reference path="[type file path]"> 语法,可以引入 .d.ts 文件

/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

现在常用的库,都已经在 npm 包里带了 type 定义文件,所以 ``/// ` 语法用的很少了。

外部模块声明简写

// declarations.d.ts

declare module "hot-new-module";

使用这个模块:

import x, {y} from "hot-new-module";
x(y);

这样写所有的导出类型都是 any。不过这可能是最常用的方案了,一般你可能都懒得去为一个第三方库写 type 定义文件, 真的挺难写的。

不过还有更简单的方法:

let x = require('hot-new-module')

对于没有 type 定义的库,直接通过 require 引入, 则 x 即为 any 类型,不需要专门去写个 definition.

加载非 js 内容

declare module "*!text" {
    const content: string;
    export default content;
}

declare module "json!*" {
    const value: any;
    export default value;
}
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

UMD 模块

UMD 模块的声明形式:

export function isPrime(x: number): boolean; // 这个库可以被当做模块使用
export as namespace mathLib;                 // 这个库可以在非模块脚本里作为全局变量来访问

创建模块结构指导

best practice for 模块的类型导出: ‘尽可能地在顶层导出’, 详见:尽可能地在顶层导出

命名空间

文件(模块)可以定义多个命名空间,来避免面重名。

namespace Module1Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  export interface ArrayValidator {
    isAcceptable(s: string): boolean;
  }
}

namespace Module2Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  export interface ArrayValidator {
    isAcceptable(s: string): boolean;
  }
}

let validator1: Module1Validation.StringValidator = null
let validator2: Module2Validation.StringValidator = null

import stringValidator = Module1Validation.StringValidator;
                  // 这个清奇的语法用于给 interface 里的内容取别名

多个同名的命令空间(可分散于不同的文件中)会被自动 merge。

// 假设你安装了 node 的 types: @types/node
namespace NodeJS {
  export let a = 100
}

class Test implements NodeJS.InspectOptions {}
console.log(NodeJS.a)

NodeJS.InspectOptions 的定义来自于 node_modules/@types/node/index.d.ts

命名空间来描述第三方全局 js 库

前面我们提到了用 module 去描述 js 写的第三方库。但是 module 描述的是模块化的库。对于一些老旧的库,他们不是模块化的,而是直接使用全局变量。此时, 可以通过外部的命名空间来声明:

比如有一个这样的 js 库:

// myLib.js
(function (window) {
  var timeout = 20
  var version = '1.0.1'

	function Cat(age) {
    this.age = age
	}
	Cat.prototype.purr = function () {
    console.log('purr')
	}

	window.myLib = {
    Cat: Cat,
    timeout: timeout,
    version: version
  }
})(window)

现在我想在 typescript 脚本里使用这个库的全局变量 myLib, 于是我需要用 namesapce 来做一个声明:

// lib.d.ts
declare namespace myLib { // 外部命名空间
  class Cat {
    constructor(age: number);

    purr(): void;
    age: number;
  }

  let timeout: number;
  const version: string;
}

引入这个声明文件,就可以用起来了:

/// <reference path="./test.d.ts"/>
let lib = new myLib.Cat(1) // OK
console.log(myLib.timeout) // OK
myLib.version = '1.0.1'    // error, version is const

无导入导出的 ts 脚本(即外部模块)中, 顶层的命令空间,都是外部命名空间。比如上一节例子中的,Module1Validation, Module2Validation

全局模块和文件模块

https://basarat.gitbooks.io/typescript/content/docs/project/modules.html

命名空间和模块对比

模块解析

具体的解析过程,这里就不记录了。如果遇到模块解析的问题,打开 traceResolution 标记分析一下就行了。相关的选项如下:

声明合并

Typescript 的实体大致分为3中,即:NamespaceTypeValue. 当我们做一个声明的时候,有时候我们同时创建了多个实体。最为典型的就是 class。下面的这个声明不仅创建了一个 Animal 类型(Type),同时也是创建了值, 即一个构造函数(Value)。

class Animal {
}

let animal: Animal;
console.log(Animal);

我们来看一下完整的声明和对应实体的关系:

Declaration Type Namespace Type Value
Namespace X   X
Class   X X
Enum   X X
Interface   X  
Type Alias   X  
Function     X
Variable     X

接口合并

相同名称的接口会被合并,合并原则如下:

命名空间与其他类型合并

扩展模块

假设有一个第三方模块 module,我们不便去修改他的代码

// module.ts
export class A {}

但是我们想给这个导出函数 A 扩展一个 test 方法,可以这样做:

// test.ts
import { A } from "./module";
declare module "./module" {
    interface A {
        test(): void
    }
}
A.prototype.test = function () {
}

最后可以这么使用:

// consumer.ts
import { A } from "./module";
import './test';

let a: A = new A();
a.test()

麻烦、令人困惑,还是少这么用吧。😂

扩展全局变量

在全局模块里这是很容易的。

   interface Array<T> {
     test(): void
   }

   Array.prototype.test = function() {}

   new Array().test()

如果你想在文件模块里给全局的类型作用于添加声明,则需要使用 declare global 语法:

    export function test() {}   // export 让这个文件变成了一个文件模块

    declare global {            // 使用 declare global
        interface Array<T> {
          test(): void
        }
    }

    Array.prototype.test = function() {}
    new Array().test()

declare global 语法让文件模块里的 interface 能像全局模块里的一样去和其他全局的定义进行 merge.

三斜线指令

/// <reference path="..." />    // 引入的额外的文件
/// <reference types="..." />   // 引入额外申明, 一般只在自己写
                                // definition 文件的时候才主动使用

这2个语法 typescript 2.0 后, 使用的越来越少了。直接通过 npm 安装申明文件后,typescript 就可以正确的引入类型文件了,一般情况下已经不需要这样语法了。

npm install --save @types/lodash

import * as _ from "lodash"; // 不在需要 reference 了。
_.padStart("Hello TypeScript!", 20, " ");

其他三斜线指令遇到的话,请参考 官方文档

混入

运行时再去实现类的成员方法的技术。 官方文档, 中文版

Decorators

面向切片编程(AOP)的核心, 装饰器, 虽然在 js 上是一个实验性的方案,但在 react.js 的 HOC 中被广泛应用。 Typescript 中,通过 experimentalDecorators 选项启用。

分为多种:

所有的装饰器实现上都是一个函数。

类装饰器

一个类装饰器的类型可表示为其类型可表示如下:

/**
 *  Ctor: 类的构造函数
 */
(Ctor: new(args: any[]) => any) => void | (new(args: any[]) => any)

如果类装饰器返回一个类,它会被用来替换原来的类。

没有返回的类装饰器

function addAge(constructor: Function) {
  constructor.prototype.age = 18;
}

@addAge
class Hello {
  name: string;
  age: number;
  constructor() {
    this.name = 'leon';
  }
}

let hello = new Hello();

console.log(hello.age);

可以认为, 类装饰器使用的语法相当于:

addAge(Hello)

返回类的类装饰器

interface IHello {
    name: string;
    age: number;
    sayHello();
}

type HelloConstructor = new(name: string, age: number) => IHello;

// 下面2种写法也可以:

// type HelloConstructor = { new(name: string, age: number): IHello };

// interface HelloConstructor {
//     new(name: string, age: number): IHello
// }

function Test(Ctor: HelloConstructor): HelloConstructor {
    return class Hoc extends Ctor {                 // 这个类装饰器,返回了一个类
        constructor(name: string, age: number) {
            super(name, age);
            this.age = age;
        }

        sayHello() {
            console.log(`hello, ${this.name}!`);
        }
   }
}

@Test
class Hello implements IHello{
  constructor(public name: string, public age: number) {}

  sayHello() {}
}

let hello = new Hello('leon', 18);
console.log(hello.age, hello.name); // 18 "leon"
hello.sayHello();                   // hello, leon!

此处的 @Test就相当于。

const Hello = Test(Hello)

实际上我们把 Hello 类已经替换掉了,不过我们保持了接口的一致性。所以我们添加的这个装饰器不影响代码的兼容性。

如果读者熟悉 react Hoc 的话,应该已经发现 react Hoc 的原理和本例类似。

方法装饰器

方法装饰器的类型可表示为:

/**
 *  target: 对于静态成员函数来说是类的构造函数,对于实例成员函数是类的原型对象
 *  propertyKey: 成员函数的名字
 *  descriptor: 成员的属性描述符
*/
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor | void

如果方法装饰器返回一个值,它会被用作该方法的属性描述符

栗子:

function enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = false;
};

function hello(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target, `hello ${propertyKey}`);
};

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable
    greet() {
        return "Hello, " + this.greeting;
    }

    @hello
    static test() {
    }
}

console.log(Object.keys(Greeter.prototype))   // [], greet is not enumerable

此例中, greet 为实例方法, 所以 enumerabletarget 参数为类的原型对象,即 Greeter.prototype, 而 test 是一个静态方法,所以 hellotarget 为构造函数,即 Greeter

访问装饰器

访问装饰器的类型表示、用法和 方法装饰器 一模一样。实质上似乎也没有啥区别。。。。

function configurable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = false;
};

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

console.log(Object.getOwnPropertyDescriptor(Point.prototype, 'x')) // { configurable: false, ... }

属性装饰器

属性装饰器的类型可表示为:

/**
 *  target: 对于静态成员函数来说是类的构造函数,对于实例成员函数是类的原型对象
 *  propertyKey: 成员函数的名字
 */
(target: any, name: propertyKey) => void | any

如果有返回值,那么似乎一定要是一个属性描述符,但是行为颇为古怪

方法装饰器相比,属性装饰器 的参数少了一个 属性描述符。为什么呢?因为没有。。。 js 中成员属性和成员方法不同,成员方法是挂在类的 prototype 对象上的, 而成员属性是在类的实例上的属性。类型 prototype 在类声明的时候就存在了,而类的实例是每次 new 之后才能存在的。装饰器都是在类创建的时候执行的,所以属性装饰器的函数参数里并不能有属性描述符, 因为没有

function test(target: any, propertyKey: string) {
    console.log(target, propertyKey);
}

class Greeter {
    @test
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
}

官网中列举的 属性装饰器 的使用实例是配合 reflect-metadata 来为属性添加元数据,这里就不介绍了。

参数装饰器

参数装饰器的类型可表示为:

/**
 *  target: 对于静态成员函数来说是类的构造函数,对于实例成员函数是类的原型对象
 *  propertyKey: 成员函数的名字
 *  index: 参数在函数参数列表中的索引
*/
(target: any, name: propertyKey, parameterIndex: number) => void | any // 如果有返回值,会被忽略

示例:

function require(target: any, propertyKey: string, parameterIndex: number) {
    console.log(target, propertyKey, parameterIndex)
}

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    greet(@require name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

官网示例里展示了如何使用参数装饰器函数装饰器去做参数的验证, 此处不深入介绍。

jsx

官方文档, 中文版

如何写声明文件

官方文档, 中文版

javascript 文件里的类型检查

可以通过加 JSDoc 注释的方式给 javascript 文件的变量指定类型。 官方文档, 中文版