自己学习看的,请不要打开,谢谢!

314 阅读31分钟

前言

掘金的产品就不能搞个QQ空间那种私密日志吗,难受的鸭匹

TypeScript yes!

基础类型简介

TypeScript支持与JavaScript几乎相同的数据类型,包括:数字,字符串,结构体,布尔值等,此外还提供了实用的枚举类型方便我们使用。

Boolean

// 布尔值
let isDone: boolean = false;

Number

除了支持十进制和十六进制字面量,还支持二进制和八进制字面量。

// 数字
let number1: number = 6;
let number2: number = 0xf00d;

String

let name: string = "pawn"

Aarry

1.在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let list: number[] = [1,2,3];

2.使用数组泛型,Array<元素类型>:

let list: Array<number> = [1,2,3];

Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同

// 声明元组类型
let x: [string, number]
// 正确的初始化
x = ["age", 22]

Enum

默认情况下,从0开始为元素编号,也可手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
console.log('c = ', c)

c = 2

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green = 4, Blue = 8}
let c: string = Color[4];
console.log('c = ', c)

c = Green

Any

为不清楚类型的变量指定一个类型

let notSure: any = 4
notSure = false
notSure = "A string"

只知道一部分数据的类型时

let list: any[] = [1, true, 'pawn'];

Void

函数没有返回值

function noReturn(params:string): void {}

Null 和 Undefined

let u: undefined = undefined
let n: null = null
n = undefined
u = null

然而,当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。

注意:我们鼓励尽可能地使用--strictNullChecks,但在本手册里我们假设这个标记是关闭的。

Never

never类型表示的是那些永不存在的值的类型

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

// 推断的返回值类型为never
function Failed() {
    return error("Fauled");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
        
    }
}

Object

object是表示非基本类型的类型, 除了number, string, boolean, symbol, null, undefined之外的类型

declare function create(o: object | null) :void ;
create({ prop: 0 })
create(null)

类型断言 As

你清楚地知道一个实体具有比它现有类型更确切的类型。

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

变量声明

let

当用let声明一个变量,它使用的是词法作用域或块作用域。拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 

重定义及屏蔽

使用var声明时,同一个变量声明n次,只会得到1个。let声明无法重复申明。

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

console.log(f4(false, 0)); // returns 0
console.log(f4(true, 0));  // returns 100

在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let重写之前的sumMatrix函数。

function sumMatrix2(matrix: number[][]) {
    let sum = 0;
    for (let i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (let i = 0; i < currentRow.length; i++) {
            sum += currentRow[i];
        }
    }

    return sum;
}

console.log("========== sumMatrix2 \n");
console.log(sumMatrix2([[1],[2]]))

运行后输出如下结果

========== sumMatrix2

3

这个版本的循环能得到正确的结果,因为内层循环的i可以屏蔽掉外层循环的i。

通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。

const 

const与let声明相似,但const被赋值后不能再改变。实际上const变量的内部状态是可修改的,但TypeScript允许你将对象的成员设置成只读的。

let vs const

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。

解构

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;

作用于函数参数:

function f6([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f6([1,2]);

在数组里使用...语法创建剩余变量:

let [one, ...rest] = [1, 2, 3, 4];

对象解构

对象也可以进行解构:

let o = {
    a: "foo",
    b: 12
};
let { a } = o;

属性重命名

这里的冒号不是指示类型的。 如果想指定它的类型, 需要在其后写上完整的模式。

let {a, b}: {a: string, b: number} = o;

默认值

默认值可以让你在属性为 undefined 时使用缺省值:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。

接口

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值

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

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

readonly vs const

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

函数类型

interface SomeInterface {
  (arg1: string, arg2: string): boolean;
}
let someFunc: SomeInterface
someFunc = function (arg1: string, arg2: string) {
    const res = arg1.search(arg2)
    return res > -1;
}

函数的参数名不需要与接口里定义的名字相匹配。

let someFunc2: SomeInterface;
someFunc2 = function (x: string, y: string): boolean {
    const res = x.search(y);
    return res > -1;
}
console.log(someFunc2('weast', 'east'));

可索引的类型

可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型

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

let someArray: SomeArray;
someArray = ["string1", "string2"];

let str: string = someArray[0];

上面例子里,定义了SomeArray接口,它具有索引签名。这个索引签名表示了当用 number去索引SomeArray时会得到string类型的返回值。共有支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。

将索引签名设置为只读,这样就防止了给索引赋值:

interface SomeInterface {
    [index: string]: string
    // length: number    // 错误,`length`的类型与索引类型返回值的类型不匹配
    name: string       // 可以,name是string类型
}

interface ReadonlySomeArray {
    readonly [index: number]: string;
}
let readonlyArray: ReadonlySomeArray = ["string1", "string2"];
readonlyArray[2] = "string3"; // error!

类类型

实现接口

interface SomeClassInterface {
    property: string;
    setProperty(p: string): any;
}

class implementSomeInterface implements SomeClassInterface {
    property: string;
    constructor(arg1: number, arg2: number) {}
    setProperty(p: string): any {
        this.property = p;
    }
}

不会检查类是否具有私有成员。

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

类是具有两个类型的:静态部分的类型和实例的类型。 应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口,SomeConstructor为构造函数所用和SomeClassInterface为实例方法所用。 为了方便我们定义一个构造函数createSomeFunc,它用传入的类型创建实例。

interface SomeClassInterface {
    property: string;
    setProperty(p: string): any;
    getProperty(): string;
}

interface SomeConstructor {
    new(arg1: string, arg2: string): SomeClassInterface
}

function createSomeFunc(ctr: SomeConstructor, arg1: string, arg2: string): SomeClassInterface {
    return new ctr(arg1, arg2)
}

class ImplementSomeInterface1 implements SomeClassInterface {
    property: string;
    property2: string;
    constructor(arg1: string, arg2: string) {
        this.property = arg1;
        this.property2 = arg2;
    }
    setProperty(p: string): any {
        this.property = p;
    }
    
    getProperty() {
        return this.property + ' = implementSomeInterface1';
    }
}

class ImplementSomeInterface2 implements SomeClassInterface {
    property: string;
    property2: string;
    constructor(arg1: string, arg2: string) {
        this.property = arg1;
        this.property2 = arg2;
    }
    setProperty(p: string): any {
        this.property = p;
    }
    
    getProperty() {
        return this.property + ' = implementSomeInterface2';
    }
}

let instance1 = createSomeFunc(ImplementSomeInterface1, 'arg1', 'arg2');
let instance2 = createSomeFunc(ImplementSomeInterface2, 'arg1', 'arg2');
console.log(instance1.getProperty());
console.log(instance2.getProperty());

因为createSomeFunc的第一个参数是SomeConstructor类型,在createSomeFunc(ImplementSomeInterface1, 'arg1', 'arg2')里,会检查ImplementSomeInterface1是否符合构造函数签名。

我觉得这里的话官方写的有点复杂了,为什么一定要使用一个构造函数接口呢,比如下面

let instance3 = new ImplementSomeInterface2('arg1','arg2')
console.log(instance3.getProperty1());

一样可以实现实例化,并且调用方法函数

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square> {};
square.color = 'red'
square.sideLength = 10;


一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square> {};
square.color = 'red'
square.sideLength = 10;
square.penWidth = 10;

混合类型

先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

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

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

let counter = getCounter()
counter(10);
counter.reset();
counter.interval = 10.0

类的使用

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

    greet() {
        return this.greeting;
    }
}

const greeter = new Greeter("Good Morning");
console.log(greeter.greet());

声明一个 Greeter类,这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法,我们在引用任何一个类成员的时候都用了 this,表示我们访问的是类的成员。

最后一行,我们使用 new构造了 Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个 Greeter类型的新对象,并执行构造函数初始化它。

继承

class Animal {
    move(distanceMeter: number = 0) {
        console.log(`Animal moved ${distanceMeter}m`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof........!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(3);
dog.bark();

类从基类中继承了属性和方法。这里,Dog是一个派生类,它派生自Animal基类,通过extends关键字。派生类通常被称作 子类,基类通常被称作超类。因为Dog继承了Animal的功能,因此我们可以创建一个Dog的实例,它能够 bark()和move()。

class Animal {
    name: string;
    constructor(theName: string) {
        this.name = theName;
    }

    move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

class Snake extends Animal {
    constructor(name: string) {
        super(name);
    }

    move(distanceMeter: number = 5) {
        console.log('滑动的声音......');
        super.move(distanceMeter);
    }
}

class Horse extends Animal {
    constructor(name: string) {
        super(name);
    }

    move(distanceMeter: number = 45) {
        console.log('跑动的声音......');
        super.move(distanceMeter);
    }
}

const snake = new Snake('small snake');
const horse: Animal = new Horse('small horse');

snake.move();
horse.move(152);

运行后得到如下结果

$ npx ts-node src/classes_2.ts
滑动的声音......
small snake moved 5m
跑动的声音......
small horse moved 152m

派生类包含了一个构造函数,它必须调用super(),它会执行基类的构造函数。 而且,在构造函数里访问this的属性之前,我们一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。Snake类和 Horse类都创建了move方法,它们重写了从Animal继承来的move方法,使得 move方法根据不同的类而具有不同的功能。注意,即使horse被声明为Animal类型,但因为它的值是Horse,调用tom.move(152)时,它会调用Horse里重写的方法。

公共,私有与受保护的修饰符

public

在TypeScript里,成员都默认为public。你也可以明确的将一个成员标记成public。 我们可以用下面的方式来实现一个Animal类:

class Animal {
    public name: string;
    public constructor(theColor: string) {
        this.name = theColor;
    }

    public move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

private

当成员被标记成 private时,它就不能在声明它的类的外部访问。

class Animal {
    private name: string;
    public constructor(theName: string) {
        this.name = theName;
    }

    public move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

new Animal('small cat').name;

TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有 private或 protected成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private成员,那么只有当另外一个类型中也存在这样一个 private成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected成员也使用这个规则。下面来看一个例子,更好地说明了这一点:

class Animal {
    private name: string;
    public constructor(theName: string) {
        this.name = theName;
    }

    public move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }
}

class Person {
    private name: string;
    
    public constructor(theName: string) {
        this.name = theName;
    }

    public move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

let animal = new Animal('animal');
let dog = new Dog('dog');
let person = new Person('person');

animal = dog
animal = person;

运行后得到如下结果

$ npx ts-node src/classes_3.ts
⨯ Unable to compile TypeScript:
src/classes_3.ts(35,1): error TS2322: Type 'Person' is not assignable to type 'Animal'.
  Types have separate declarations of a private property 'name'.

这个例子中有Animal和Dog两个类, Dog是 Animal类的子类。还有一个Person类,其类型看上去与Animal是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。因为Animal和Dog共享了来自Animal里的私有成员定义private name: string,因此它们是兼容的。 然而 Person却不是这样。当把Person赋值给Animal的时候,得到一个错误,说它们的类型不兼容。尽管Person里也有一个私有成员name,但它明显不是Animal里面定义的那个。

protected

protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。

class Person {
    protected name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    getWorkInfo() {
        return `我叫${this.name},我工作在${this.department}`;
    }
}

let aEmployee = new Employee('durban', '华盛顿');
console.log(aEmployee.getWorkInfo());
console.log(aEmployee.name);

运行后得到的结果如下

$ npx ts-node src/classes_3.ts
⨯ Unable to compile TypeScript:
src/classes_3.ts(23,23): error TS2445: Property 'name' is protected and only accessible within class 'Person' and its subclasses.

注意,我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为 Employee是由 Person派生而来的。构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如:

class Person {
    protected name: string;
    protected constructor(name: string) {
        this.name = name;
    }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    getWorkInfo() {
        return `我叫${this.name},我工作在${this.department}`;
    }
}

let aEmployee = new Employee('durban', '华盛顿');
let aPerson = new Person('Sakuro');

运行后得到如下错误

$ npx ts-node src/classes_3.ts
⨯ Unable to compile TypeScript:
src/classes_3.ts(22,15): error TS2674: Constructor of class 'Person' is protected and only accessible within the class declaration.

readonly修饰符

你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。如下:

class Person {
    readonly name: string;
    constructor(name: string) {
        this.name = name;
    }
}

const aPerson = new Person('Xiaowang');
aPerson.name = 'Xiaoli';

运行后得到如下

$ npx ts-node src/classes_3.ts
⨯ Unable to compile TypeScript:
src/classes_3.ts(9,9): error TS2540: Cannot assign to 'name' because it is a constant or a read-only property.

参数属性

在上面的例子中,我们不得不定义一个受保护的成员 name和一个构造函数参数 theName在 Person类里,并且立刻给 name和 theName赋值。 这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Animal类的修改版,使用了参数属性:

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

    public move(distanceMeter: number = 0) {
        console.log(`${this.name} moved ${distanceMeter}m`);
    }
}

注意看我们是如何舍弃了 theName,仅在构造函数里使用 private name: string参数来创建和初始化 name成员。 我们把声明和赋值合并至一处。

参数属性通过给构造函数参数添加一个访问限定符来声明。 使用 private限定一个参数属性会声明并初始化一个私有成员;对于 public和 protected来说也是一样。

存取器

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。下面来看如何把一个简单的类改写成使用 get和 set。 首先,我们从一个没有使用存取器的例子开始

class Employee {
    fullName: string    
}

let employee = new Employee();
employee.fullName = "durban zhang";

if (employee.fullName) {
    console.log(employee.fullName);
}

运行后结果如下

$ npx ts-node src/classes_4.ts
durban zhang

我们可以随意的设置 fullName,这是非常方便的,但是这也可能会带来麻烦。

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName的直接访问改成了可以检查密码的 set方法。 我们也加了一个 get方法,让上面的例子仍然可以工作。

let passcode = 'pass';
class Employee {
    private _fullName: string;

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

    set fullName(name: string) {
        if (passcode && passcode === 'pass') {
            this._fullName = name;
        } else {
            console.log('授权失败');
        }
    }
}

let employee = new Employee();
employee.fullName = "durban zhang";

if (employee.fullName) {
    console.log(employee.fullName);
}


运行后结果如下

$ npx ts-node src/classes_4.ts
durban zhang

可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。

对于存取器有下面几点需要注意的:

首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get不带有 set的存取器自动被推断为 readonly。 这在从代码生成 .d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

静态属性

到目前为止,只学习了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static定义 origin,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin前面加上类名。 如同在实例属性上使用 this.前缀来访问属性一样,这里我们使用 Grid.来访问静态属性。

class Grid {
    constructor(public scale: number) { }

    static origin = {
        x:0,
        y:0,
    }

    calculateDistanceFromOrigin(point: {x: number, y: number}) {
        let xDist = point.x - Grid.origin.x;
        let yDist = point.y - Grid.origin.y;

        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
}

let grid1 = new Grid(1.0)
let grid2 = new Grid(2.0)

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));

运行后结果如下

14.142135623730951
7.0710678118654755

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。具体例子如下

abstract class Animal {
    abstract makeSount(): void;    move(): void {
        console.log('我在移动');
    }
}


抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。具体示例如下

abstract class Department {
    constructor(public name: string) {

    }

    printName(): void {
        console.log("部门名称:" + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现   
}

class AccountingDepartment extends Department {
    constructor() {
        super("会计和审计"); // 在派生类中必须调用super()
    }

    printMeeting(): void {
        console.log('会计部每个星期一上午10点开会');
    }

    generateReports(): void {
        console.log('生成会议报告');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用

// department = new Department(); // 不能创建一个抽象类的实例
department = new AccountingDepartment(); //  允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
// department.generateReports(); //  此方法不能调用,因为在声明的抽象类中不存在


运行后的结果如下

$ npx ts-node src/classes_6.ts
部门名称:会计和审计
会计部每个星期一上午10点开会

构造函数

当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。

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

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

let greeter: Greeter;
greeter = new Greeter('Gowhich');
console.log(greeter.greet());

运行后得到如下结果

$ npx ts-node src/classes_7.ts
Hello, Gowhich

这里,我们写了 let greeter: Greeter,意思是 Greeter类的实例的类型是 Greeter。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。

我们也创建了一个叫做 构造函数的值。 这个函数会在我们使用 new创建类实例的时候被调用。 下面我们来看看,上面的代码被编译成JavaScript后是什么样子的,如下所示

var Greeter = /** @class */ (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
}());
var greeter;
greeter = new Greeter('Gowhich');
console.log(greeter.greet());

上面的代码里, var Greeter将被赋值为构造函数。 当我们调用 new并执行了这个函数后,便会得到一个类的实例。 这个构造函数也包含了类的所有静态属性。 换个角度说,我们可以认为类具有 实例部分与 静态部分这两个部分。让我们稍微改写一下这个例子,看看它们之间的区别:

class Greeter {
    static standardGreeting: string = 'Hello, World';
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, "+this.greeting;
        } else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = 'Hello, other';

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

运行后得到的结果如下

$ npx ts-node src/classes_7.ts
Hello, World
Hello, other

这个例子里, greeter1与之前看到的一样。 我们实例化 Greeter类,并使用这个对象。 与我们之前看到的一样。

再之后,我们直接使用类。 我们创建了一个叫做 greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,"告诉我 Greeter标识符的类型",也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker上使用 new,创建 Greeter的实例。

把类当做接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。

class Point {
    x: number;
    y: number;
}

interface Point3D extends Point {
    z: number;
}

let point3d: Point3D = {
    x: 1,
    y: 2,
    z: 3,
}

console.log('point3d = ', point3d);

运行后得到的结果如下

$ npx ts-node src/classes_7.ts
point3d =  { x: 1, y: 2, z: 3 }

函数

函数类型

为函数定义类型
让我们为上面那个函数添加类型

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

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

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。

书写完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

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

函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写

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

只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。

第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用( =>)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void而不能留空。

函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。

推断类型

尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:

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


这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。

可选参数和默认参数

TypeScript里的每个函数参数都是必须的。 这不是指不能传递 null或undefined作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。如下实例演示

function buildName(firstName: string, lastName: string) {
    return firstName + ' ' + lastName
}

// 错误演示
buildName("firstName")
// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName", "lastName")

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

function buildName(firstName: string, lastName?: string) {
    return firstName + ' ' + lastName
}

// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName")
// 正确演示
buildName("firstName", "lastName")

可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"。

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

// 正确演示
buildName("A")
// 正确演示
buildName("A", undefined)
// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName", "lastName")


在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。

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

function buildName(firstName: string, lastName?: string) {
    return firstName + ' ' + lastName
}

共享同样的类型(firstName: string, lastName?: string) => string。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined值来获得默认值。 例如,我们重写最后一个例子,让 firstName是带默认值的参数:

function buildName(firstName="Durban", lastName: string) {
    return firstName + ' ' + lastName
}

// 错误演示
buildName("A")
// 正确演示
buildName(undefined,  "A")
// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName", "lastName")

剩余参数

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

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

let aName = buildName("Lili", "John", "David", "Durban");
console.log(aName);

运行后得到的结果如下

$ npx ts-node src/function_3.ts
Lili John David Durban

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号(...)后面给定的名字,你可以在函数体内使用这个数组。这个省略号也会在带有剩余参数的函数类型定义上使用到:

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

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

console.log(buildNameFunc("John", "Julia", "July"));

运行后得到的结果如下

$ npx ts-node src/function_3.ts
Lili John David Durban

this

学习如何在JavaScript里正确使用this就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清 this工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了 this的地方。 如果你想了解JavaScript里的 this是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了 this的内部工作原理,因此这里只做简单介绍。

this和箭头函数

JavaScript里,this的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。

下面看一个例子:

let deck = {
    suits: [
        'hearts',
        'spades',
        'clubs',
        'diamods'
    ],
    cards: Array(52),
    createCardPicker: function () {
        return function () {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {
                suit: this.suits[pickedSuit], card: pickedCard % 13,
            }
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

可以看到createCardPicker是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会得到如下类似错误提示

$ npx ts-node src/function_4.ts
Cannot read property '2' of undefined

因为 createCardPicker返回的函数里的this被设置成了window而不是deck对象。 因为我们只是独立的调用了 cardPicker()。 顶级的非方法式调用会将 this视为window。 (注意:在严格模式下, this为undefined而不是window)。为了解决这个问题,我们可以在函数被返回时就绑好正确的this。 这样的话,无论之后怎么使用它,都会引用绑定的'deck'对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 this值,而不是调用时的值:

let deck = {
    suits: [
        'hearts',
        'spades',
        'clubs',
        'diamods'
    ],
    cards: Array(52),
    createCardPicker: function() {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {
                suit: this.suits[pickedSuit],
                card: pickedCard % 13,
            }
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

运行后得到的结果如下

$ npx ts-node src/function_4.ts
card: 10 of hearts

TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis标记。 它会指出 this.suits[pickedSuit]里的this的类型为any。

this参数

this.suits[pickedSuit]的类型依旧为any。 这是因为 this来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this参数。 this参数是个假的参数,它出现在参数列表的最前面,如下

function f(this: void) {
    // 确保`this`在这个独立功能中无法使用
}

我们添加一些接口,Card 和 Deck,让类型重用能够变得清晰简单些,代码如下

interface Card {
    suit: string;
    card: number;
}

interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
    suits: [
        'hearts',
        'spades',
        'clubs',
        'diamods'
    ],
    cards: Array(52),
    createCardPicker: function (this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {
                suit: this.suits[pickedCard],
                card: pickedCard % 13,
            }
        }
    }

}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);

运行后得到的结果类似如下

$ npx ts-node src/function_5.ts
card: 3 of diamods


现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说 this是Deck类型的,而非any,因此--noImplicitThis不会报错了。

this参数在回调函数里

我们也看到过在回调函数里this报错的情况,当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this将为undefined。 稍做改动,你就可以通过 this参数来避免错误。 首先,库函数的作者要指定 this的类型,如下实例

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: void表示addClickListener期望onclick是一个不需要此类型的函数。
其次,用这个注释你的调用代码,如下所示

interface UIElement {
    addClickListener(onclick: (this: void, e: Error) => void): void;
}

class Handler {
    info: string;
    onClickBad(this: Handler, e: Error) {
        // oops, used this here. using this callback would crash at runtime
        this.info = e.message;
    }
}
let h = new Handler();
let uiElement: UIElement = {
    addClickListener(onclick: (this: void, e: Error) => void) {
        // do something
    }
};

uiElement.addClickListener(h.onClickBad); // 这里会报错

指定了this类型后,显式声明onClickBad必须在Handler的实例上调用。 然后TypeScript会检测到 addClickListener要求函数带有this: void。 我们添加另外一个函数做下对比,如下

interface UIElement {
    addClickListener(onclick: (this: void, e: Error) => void): void;
}

class Handler {
    info: string;
    onClickBad(this: Handler, e: Error) {
        this.info = e.message;
    }
    onClickGood(this: void, e: Error) {
        console.log('点击了!');
    }
}
let h = new Handler();
let uiElement: UIElement = {
    addClickListener(onclick: (this: void, e: Error) => void) {
        // do something
    }
};

uiElement.addClickListener(h.onClickGood);

通过将h.onClickBad更换为h.onClickGood,就能正常调用。
因为onClickGood指定了this类型为void,因此传递addClickListener是合法的。 当然了,这也意味着不能使用 this.info. 如果你两者都想要,你不得不使用箭头函数了,如下

interface UIElement {
    addClickListener(onclick: (this: void, e: Error) => void): void;
}

class Handler {
    info: string;
    onClickGood = (e: Error) => { this.info = e.message }
}

let h = new Handler();
let uiElement: UIElement = {
    addClickListener(onclick: (this: void, e: Error) => void) {
        // do something
    }
};

uiElement.addClickListener(h.onClickGood);

这是可行的因为箭头函数不会捕获this,所以你总是可以把它们传给期望this: void的函数。 缺点是每个 Handler对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到 Handler的原型链上。 它们在不同 Handler对象间是共享的。

重载

JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。如下实例

let suits = ["hearts", "spades", "clubs", "diamonds"];


function pickCard(x: any): any {
    if (typeof x == "object") {
        let pickCard = Math.floor(Math.random() * x.length);
        return pickCard;
    } else if (typeof x == 'number') {
        let pickedSuit = Math.floor(x / 13);
        return {
            suit: suits[pickedSuit],
            card: x % 13,
        }
    }
}

let myDeck = [
    {
        suit: "diamands",
        card: 2,
    },
    {
        suit: 'spades',
        card: 10,
    },
    {
        suit: 'hearts',
        card: 4
    }
]

let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);

console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit);
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);

运行后得到类型如下结果

$ npx ts-node src/function_7.ts
card: 2 of diamands
card: 2 of spades

pickCard方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 pickCard函数。

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string, card: number}[]): number;
function pickCard(x: number): {suit: string, card: number};
function pickCard(x: any): any {
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    } else if (typeof x == 'number') {
        let pickedSuit = Math.floor(x / 13);
        return {
            suit: suits[pickedSuit],
            card: x % 13,
        }
    }
}

let myDeck = [
    {
        suit: "diamands",
        card: 2,
    },
    {
        suit: 'spades',
        card: 10,
    },
    {
        suit: 'hearts',
        card: 4
    }
]

let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);

console.log('card: ' + pickedCard1.card + ' of ' + pickedCard1.suit);
console.log('card: ' + pickedCard2.card + ' of ' + pickedCard2.suit);

得到的结果类似如下

$ npx ts-node src/function_7.ts
card: 10 of spades
card: 2 of spades

这样改变后,重载的pickCard函数在调用的时候会进行正确的类型检查。

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

注意,function pickCard(x: any): any并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard会产生错误。

泛型

泛型的简单使用

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是 echo命令。

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

或者

function identity(arg: any): any {
    return arg
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 "类型变量",它是一种特殊的变量,只用于表示类型而不是值。

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

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数,如下:

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

let output = identity<string>("someString");
console.log(output);
console.log(typeof output);

运行后输出结果如下

$ npx ts-node src/generics_1.ts
someString
string

这里我们明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型,如下

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

let output = identity("other someString");
console.log(output);
console.log(typeof output);


运行后输出结果如下

$ npx ts-node src/generics_1.ts
someString
string

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看

other someString

的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量

使用泛型创建像上篇分享提到的identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

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

如果我们想同时打印出arg的长度。 我们很可能会这样做:

function loggingIdentity<T>(arg: T): T {
    // console.log(arg.length); // no length property
    return arg
}

如果这么做,编译器会报错说我们使用了arg的.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);
    return arg;
}

使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一次的分享,会介绍如何创建自定义泛型像 Array<T>一样。

泛型类型

上一篇文章的分享,我们创建了identity通用函数,可以适用于不同的类型。 在这次分享中分享一下函数本身的类型,以及如何创建泛型接口。泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样,如下

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

let otherIdentity: <T> (arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以,如下

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

let other1Identity: <U> (arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数,如下

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

let other2Identity: { <U>(arg: U): U } = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口,如下

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

interface GenerateIdentityFunc {
    <U> (arg: U): U;
}

let other3Identity: GenerateIdentityFunc = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如:

Dictionary<string>

而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

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

interface GenerateIdentityFunc1<U> {
    (arg: U): U
}

let other4Identity: GenerateIdentityFunc1<number> = identity;

注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用 GenerateIdentityFunc1的时候,还得传入一个类型参数来指定泛型类型(这里是:

number

),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面。

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

let generateT1 = new GenerateT<number>();
generateT1.zeroValue = 1
generateT1.add = (x, y) => { return x + y; }

GenerateNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

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

let generateT2 = new GenerateT<string>();
generateT2.zeroValue = "";
generateT2.add = (x, y) => { return x + y; }

console.log(generateT2.add(generateT2.zeroValue, "test"));


与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束

我之前分享的一个例子中,有时候想操作某类型的一组值,并且知道这组值具有什么样的属性。在loggingIdentity例子中,我们想访问arg的length属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}


相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface LengthDefine {
    length: number;
}

function loggingIdentity<T extends LengthDefine>(arg: T): T {
    console.log(arg.length);

    return arg;
}


现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);


运行后会遇到如下错误提示

⨯ Unable to compile TypeScript:
src/generics_5.ts(11,17): error TS2345: Argument of type '3' is not assignable to parameter of type 'LengthDefine'.


我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value:3});

运行后会得到如下结果

$ npx ts-node src/generics_5.ts
10

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

我们可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

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

let x = {a:1, b:2, c:3, d:4};
getProperty(x, "a"); // 正常
getProperty(x, "m"); // 异常

运行后得到如下错误信息

$ npx ts-node src/generics_5.ts
⨯ Unable to compile TypeScript:
src/generics_5.ts(21,16): error TS2345: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

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

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

class Keeper1 {
    hasMask: boolean;
}

class Keeper2 {
    nameTag: string;
}

class Keeper3 {
    numLength: number;
}

class ChildrenKeeper1 extends Keeper3 {
    keeper: Keeper1;
}


class ChildrenKeeper2 extends Keeper3 {
    keeper: Keeper2;
}

function createInstance<A extends Keeper3> (c: new() => A): A {
    return new c();
}

console.log(createInstance(ChildrenKeeper1));
console.log(createInstance(ChildrenKeeper2));

运行后得到如下输出

$ npx ts-node src/generics_5.ts
ChildrenKeeper1 {}
ChildrenKeeper2 {}


感觉没在实际应用中使用,很鸡肋呀

枚举

数字枚举

首先我们看看数字枚举,如果你使用过其它编程语言应该会很熟悉。

enum Derection {
    Up = 1,
    Down,
    Left,
    Right
}


如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。

我们还可以完全不使用初始化器,如下

enum Derection {
    Up,
    Down,
    Left,
    Right
}


现在, Up的值为 0, Down的值为 1等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。
使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型,如下示例

enum ResponseOther {
    No = 0,
    Yes = 1,
}

function respond(re: string, me: ResponseOther) {
    // other doing
}

respond("message", ResponseOther.No)


数字枚举可以被混入到 计算过的和常量成员(如下所示)。 简短地说,不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。 换句话说,下面的情况是不被允许的:

enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

字符串枚举

字符串枚举的概念很简单,但是有细微的 运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}


由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。 换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的 - 它并不能表达有用的信息(尽管 反向映射会有所帮助),字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。

异构枚举

从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}


除非你真的想要利用JavaScript运行时的行为,否则我们不建议这样做。

计算的和常量成员

每个枚举成员都带有一个值,它可以是 常量或 计算出来的。 当满足如下条件时,枚举成员被当作是常量:

它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0:

// E.X is constant:
enum E { X }

它不带有初始化器且它之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加1。

enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}

枚举成员使用 常量枚举表达式初始化。 常数枚举表达式是TypeScript表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式:

  • 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
  • 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
  • 带括号的常量枚举表达式
  • 一元运算符 +, -, ~其中之一应用在了常量枚举表达式
  • 常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^的操作对象。 若常数枚举表达式求值后为 NaN或 Infinity,则会在编译阶段报错。

所有其它情况的枚举成员被当作是需要计算得出的值。

enum FileAccess {
    // 常量
    None,
    Read= 1 << 1,
    Write = 1 << 2,
    ReadWrite = Read | Write,
    // 计算出来的
    G = "123".length,
}

联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为

  • 任何字符串字面量(例如: "foo", "bar", "baz")
  • 任何数字字面量(例如: 1, 100)
  • 应用了一元 -符号的数字字面量(例如: -1, -100)

当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。

首先,枚举成员成为了类型! 例如,我们可以说某些成员 只能是枚举成员的值:

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle,
    radius: number,
}

interface Square {
    kind: ShapeKind.Square,
    sideLength: number,
}

let c: Circle = {
    kind: ShapeKind.Square,
    redius: 100,
}


运行后会有如下错误提示

$ npx ts-node src/generics_8.ts
⨯ Unable to compile TypeScript:
src/generics_8.ts(17,5): error TS2322: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.


另一个变化是枚举类型本身变成了每个枚举成员的 联合。 虽然我们还没有讨论[联合类型](./Advanced Types.md#union-types),但你只要知道通过联合枚举,类型系统能够利用这样一个事实,它可以知道枚举里的值的集合。 因此,TypeScript能够捕获在比较值的时候犯的愚蠢的错误。 例如:

enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        
    }
}


运行后会有如下错误提示

$ npx ts-node src/generics_8.ts
⨯ Unable to compile TypeScript:
src/generics_8.ts(27,23): error TS2367: This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.


这个例子里,我们先检查 x是否不是 E.Foo。 如果通过了这个检查,然后 ||会发生短路效果, if语句体里的内容会被执行。 然而,这个检查没有通过,那么 x则 只能为 E.Foo,因此没理由再去检查它是否为 E.Bar。

运行时的枚举

枚举是在运行时真正存在的对象。 例如下面的枚举:

enum E {
    X, Y, Z
}

实际上可以传递给函数

enum E {
    X,Y,Z
}
function f(obj: { X: number }) {
    return obj.X
}

console.log(f(E));

反向映射

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:

enum Enum {
    A,
}

let a = Enum.A;
let nameOfA = Enum[a];
console.log(nameOfA);

TypeScript可能会将这段代码编译为下面的JavaScript:

var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a];
console.log(nameOfA);

生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。

要注意的是 不会为字符串枚举成员生成反向映射。

const枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义。

const enum Enum {
    A = 1,
    B = A * 2,
}

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。

const enum Directions {
    Up,
    Down,
    Left,
    Right,
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];


生成后的代码为:

var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

外部枚举用来描述已经存在的枚举类型的形状。

declare enum Enum {
    A = 1,
    B,
    C = 2
}


外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。