TypeScript学习笔记

358 阅读13分钟

什么是 TypeScript

引用官网的定义:

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Any browser. Any host. Any OS. Open source.

翻译成中文即是:

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。


安装TypeScript

  • 全局安装
    npm install -g typescript
  • 编译 TypeScript
    tsc hello.ts
    使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。

TS基础

原始数据类型

布尔值

let isDone: boolean = false;

数值

// number
let age: number = 123
// ES6 中的二进制表示法
let binaryNumber: number = 0b111
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;

字符串

// string
let firstName: string = 'jake'
let message: string = `hello, ${firstName},age is ${age}`

空值

JavaScript 没有空值(Void)的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

// void
function alertName(): void {
    alert('My name is Tom');
}

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull

let unusable: void = undefined;

Null 和 Undefined

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

void 的区别是,undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

let num: number = undefined
// 这样也不会报错
let u: undefined;
let num: number = u;

void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.




类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

什么是类型推论

以下代码虽然没有指定类型,但是会在编译的时候报错:

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

事实上,它等价于:

let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:

let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;





联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。(ps:就是为类型的并集)

let numberOrString: number | string = 234
numberOrString = 'abc'

联合类型使用 | 分隔每个类型。 这里的 let numberOrString: string | number 的含义是,允许 numberOrString 的类型是 string 或者 number,但是不能是其他类型。

联合类型的属性或方法:

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

function getLength(something: string | number): number {
    return something.length;
}

// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

上例中,length属性不是stringnumber的共有属性,所以会报错。可以访问它们的共有属性

function getString(something: string | number): string {
    return something.toString()
}

联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错

// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.

上例中,第二行的 myFavoriteNumber 被推断成了 string,访问它的 length 属性不会报错。 而第四行的 myFavoriteNumber 被推断成了 number,访问它的 length 属性时就报错了。




任意值

任意值(Any)用来表示允许赋值为任意类型,即对所赋值类型没有要求。

// any类型
let notSure: any = 4
notSure = `maybe it is a string`
notSure = true

在任意值上访问任何属性都是允许的

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

也允许调用任何方法

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');

声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型。






数组类型

在 TypeScript 中,数组类型有多种定义方式,比较灵活。 最简单的方法是使用「类型 + 方括号」来表示数组:

let arrOfNumber: number[] = [1, 2, 3, 4]

数组的项中不允许出现其他的类型:

let arrOfNumber: number[] = [1, 'str', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.

数组的一些方法的参数也会根据数组在定义时约定的类型进行限制

let arrOfNumber: number[] = [1, 1, 2, 3, 5];
arrOfNumber.push('str');

// Argument of type '"str"' is not assignable to parameter of type 'number'.

上例中,push 方法只允许传入 number 类型的参数,但是却传了一个 "8" 类型的参数,所以报错了。这里"str"是一个字符串字面量类型,会在后续章节中详细介绍。

(随着学习的深入还会继续补充)






元组

数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。

定义一对值分别为 stringnumber 的元组:

let tom: [string, number] = ['Tom', 25];

当赋值或访问一个已知索引的元素时,会得到正确的类型:

let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

也可以只赋值其中一项:

let tom: [string, number];
tom[0] = 'Tom';

当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。

let tom: [string, number];
tom = ['Tom', 25];
let tom: [string, number];
tom = ['Tom'];

// Property '1' is missing in type '[string]' but required in type '[string, number]'.

当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:

let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.





枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,红绿灯颜色限定为红绿蓝等。

枚举使用 enum 关键字来定义:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days.Sun === 0); // true
console.log(Days.Mon === 1); // true
console.log(Days.Tue === 2); // true
console.log(Days.Sat === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

下枚举的例子:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

会编译成:

var Days;
(function (Days) {
    Days[Days["Sun"] = 0] = "Sun";
    Days[Days["Mon"] = 1] = "Mon";
    Days[Days["Tue"] = 2] = "Tue";
    Days[Days["Wed"] = 3] = "Wed";
    Days[Days["Thu"] = 4] = "Thu";
    Days[Days["Fri"] = 5] = "Fri";
    Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
;

手动赋值

我们也可以给枚举项手动赋值:

数值枚举
enum Direction  {
    Up = 10,
    Down,
    Left,
    Right
}
console.log(Direction.Up)       // 10
console.log(Direction.Down)     // 11
console.log(Direction.Left)     // 12
console.log(Direction.Right)    // 13   

上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增。

如果未手动赋值的枚举项与手动赋值的重复了,TypeScript 是不会察觉到这一点的:

enum Direction  {
    Up = 10,
    Down,
    Left = 10,
    Right
}
console.log(Direction.Up)       // 10
console.log(Direction.Down)     // 11
console.log(Direction.Left)     // 10
console.log(Direction.Right)    // 11  

所以使用的时候需要注意,最好不要出现这种覆盖的情况。

字符串枚举
enum Direction  {
    Up,
    Down,
    Left = 'LEFT',
    Right = 'RIGHT'
}

从开始赋值的位置之后都需要赋值

它可以使用简单的字符串比较,比如服务器返回的值可能是UP DOWN LEFT RIGHT,服务器返回的值可以与枚举进行比较,处理不同的逻辑:

enum Direction  {
    Up = 'UP',
    Down = 'DOWN', 
    Left = 'LEFT',
    Right = 'RIGHT'
}
const value = 'UP'
if(value === Direction.Up) {
    console.log('go up!')
}

上例,当值为UP时,处理相应的逻辑

常量枚举

常量枚举举是使用 const enum 定义的枚举类型:

const enum Direction  {
    Up = 'UP',
    Down = 'DOWN', 
    Left = 'LEFT',
    Right = 'RIGHT'
}
const value = 'UP'
if(value === Direction.Up) {
    console.log('go up!')
} 

编译后:

var value = 'UP';
if (value === "UP" /* Up */) {
    console.log('go up!');
}
常量枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。





对象的类型——接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

什么是接口(Interface)

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

例子

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

let jake: Person = {
    id: 1234,
    name: 'jake',
    age: 18
}

上面的例子中,我们定义了一个接口 Person,接着定义了一个变量 jake,它的类型是 Person。这样,我们就约束了 jake 的形状必须和接口 Person 一致。

定义的变量比接口少了一些属性是不允许的

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

let jake: Person = {
    id: 1234,
    name: 'jake',
}

// index.ts(7,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
//   Property 'age' is missing in type '{ name: string; }'.

多一些属性也是不允许的:

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

let jake: Person = {
    name: 'jake',
    age: 18,
    gender: 'male'
};

// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

赋值的时候,变量的形状必须和接口的形状保持一致。(ps:即变量的属性与类型必须接口的属性与类型一一对应)注意接口中定义属性时为;号,而不是,

可选属性

interface Person {
    readonly id: number;
    name: string;
    age?: number;
}

let jake: Person = {
    id: 1234,
    name: 'jake',
}

let jake: Person = {
    id: 1234,
    name: 'jake',
    age: 18
}

上面的例子age为可选属性,可选属性的含义是该属性可以不存在。

仍然不允许添加未定义的属性

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

let jake: Person = {
    name: 'jake',
    age: 25,
    gender: 'male'
};

// examples/playground/index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.

任意属性

interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let jake: Person = {
    name: 'jake',
    gender: 'male'
};

使用 [propName: string] 定义了任意属性取 string 类型的值。 需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let jake: Person = {
    name: 'jake',
    age: 25,
    gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

上述任意属性允许的类型是 string ,而可选的 age 属性为number所以报错

一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let jake: Person = {
    name: 'jake',
    age: 25,
    gender: 'male'
};

只读属性

即对象中的一些字段只能在创建的时候被赋值(属性值不想被修改),那么可以用 readonly 定义只读属性:

interface Person {
    readonly id: number;
    name: string;
    age?: number;
}

let jake: Person = {
    id: 123 4,
    name: 'jake',
}
jake.id = 9527;

// index.ts(11,6): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

定义为只读属性之后,必须将其赋值,刚好弥补const关键字,const是定义变量的值为常量(对于对象的属性const其实无能为力),而readonly定义的是对象中属性的常量,对象属性的值无法改变




函数的类型

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

// 函数声明(Function Declaration)
function sum(x, y) {
    return x + y;
}

// 函数表达式(Function Expression)
let mySum = function (x, y) {
    return x + y;
};

在 TypeScript 中对其进行约束,需要把输入和输出都考虑到(ps:即需将传入参数与函数返回值做类型声明)

函数声明

其中函数声明的类型定义较简单:

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

注意,输入多余的(或者少于要求的)参数,是不被允许的:

function sum(x: number, y: number): number {
    return x + y;
}
sum(1, 2, 3);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
function sum(x: number, y: number): number {
    return x + y;
}
sum(1);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

函数表达式

如果要我们现在写一个对函数表达式(Function Expression)的定义,可能会写成这样:

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

这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义(为number),而等号左边的mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:

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

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>
在 TypeScript 的类型定义中,=>用来表示函数的类型声明,左边是输入类型,需要用括号括起来,右边是输出类型。而不是ES6中的箭头函数。

用接口定义函数的形状

我们也可以使用接口的方式来定义一个函数需要符合的形状:

interface SearchFunc {
    (source: string, subString: string): boolean;
}

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

采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。






传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,才有class。(ps:用法感觉跟Java一样)

类的概念

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
    使用 class 定义类,使用 constructor 定义构造函数。 通过 new 生成新实例的时候,会自动调用构造函数。

    class Animal {
        constructor(name) {
            this.name = name;
        }
        sayHi() {
            return `My name is ${this.name}`;
        }
    }
    
    let a = new Animal('Jack');
    console.log(a.sayHi()); // My name is Jack
    
  • 对象(Object):类的实例,通过 new 生成

  • 面向对象(OOP)的三大特性:封装、继承、多态

  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据

  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性

    class Cat extends Animal {
        constructor(name) {
            super(name); // 调用父类的 constructor(name)
            console.log(this.name);
        }
        sayHi() {
            return 'Meow, ' + super.sayHi(); // 调用父类的 sayHi()
        }
    }
    
    let c = new Cat('Tom'); // Tom
    console.log(c.sayHi()); // Meow, My name is Tom
    
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 CatDog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat

  • 存取器
    使用 getter 和 setter 可以改变属性的赋值和读取行为:(set,get方法)

    class Animal {
        constructor(name) {
            this.name = name;
        }
        get name() {
            return 'Jack';
        }
        set name(value) {
            console.log('setter: ' + value);
        }
    }
    
    let a = new Animal('Kitty'); // setter: Kitty
    a.name = 'Tom'; // setter: Tom
    console.log(a.name); // Jack
    
  • 静态方法
    使用 static 修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用:

    class Animal {
        static isAnimal(a) {
            return a instanceof Animal;
        }
    }
    
    let a = new Animal('Jack');
    Animal.isAnimal(a); // true
    a.isAnimal(a); // TypeError: a.isAnimal is not a function
    

ES7 中类的用法

实例属性

ES6 中实例的属性只能通过构造函数中的 this.xxx 来定义,ES7 提案中可以直接在类里面定义:

class Animal {
    name = 'Jack';

    constructor() {
        // ...
    }
}

let a = new Animal();
console.log(a.name); // Jack

静态属性

ES7 提案中,可以使用 static 定义一个静态属性:

class Animal {
    static num = 42;

    constructor() {
        // ...
    }
}

console.log(Animal.num); // 42

TypeScript 中类的用法

public private 和 protected

个人感觉与Java类似

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的。

    class Animal {
        public name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    let a = new Animal('Jack');
    console.log(a.name); // Jack
    a.name = 'Tom';
    console.log(a.name); // Tom
    

    以上的例子name设置为public,所以直接访问实例的name属性是被是允许的。

  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问

  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

    很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private 了:

    class Animal {
        private name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    let a = new Animal('Jack');
    console.log(a.name); // Jack
    a.name = 'Tom'; // 不能修改name属性
    
    // index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
    // index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
    

    需要注意的是,TypeScript 编译之后的代码中,并没有限制 private 属性在外部的可访问性。
    上面的例子编译后的代码是:

    var Animal = (function () {
        function Animal(name) {
            this.name = name;
        }
        return Animal;
    }());
    var a = new Animal('Jack');
    console.log(a.name);
    a.name = 'Tom';
    

    使用 private 修饰的属性或方法,在子类中也是不允许访问的:

    class Animal {
        private name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    class Cat extends Animal {
        constructor(name) {
            super(name);
            console.log(this.name);
        }
    }
    
    // index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
    

    而如果是用 protected 修饰,则允许在子类中访问:

    class Animal {
        protected name;
        public constructor(name) {
            this.name = name;
        }
    }
    
    class Cat extends Animal {
        constructor(name) {
            super(name);
            console.log(this.name);
        }
    }
    

    当构造函数修饰为 private 时,该类不允许被继承或者实例化:

    class Animal {
        public name;
        private constructor (name) {
            this.name = name;
      }
    }
    class Cat extends Animal {
        constructor (name) {
            super(name);
        }
    }
    
    let a = new Animal('Jack');
    
    // index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
    // index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.
    

    当构造函数修饰为 protected 时,该类只允许被继承

    class Animal {
        public name;
        protected constructor (name) {
            this.name = name;
      }
    }
    class Cat extends Animal {
        constructor (name) {
            super(name);
        }
    }
    
    let b = new Cat('Jack');
    let a = new Animal('Jack'); // 报错
    
    // index.ts(14,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.
    

readonly:

只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。

class Animal {
    readonly name;
    public constructor(name) {
        this.name = name;
    }
}

let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';

// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.

如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。

class Animal {
    // public readonly name;
    public constructor(public readonly name) {
        // this.name = name;
    }
}





类与接口

类实现接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,我们实现两个类CarCellPhone,手机和汽车两个类都有开关收音机的功能,这是就可以考虑把共有的功能提取出来,作为一个接口,让CarCellPhone类去实现它:

interface Radio {
    switchRadio(flag: boolean): void
}

class Car implements Radio {
    switchRadio() {}
}

class CellPhone  implements Radio {
    switchRadio() {}
}

一个类可以实现多个接口,比如手机可以查看电量,而汽车没有这个功能,我们就可以将电量作为手机的另一个接口:

interface Radio {
    switchRadio(flag: boolean): void
}

interface Battery {
    checkBatteryStauts();
}

class CellPhone  implements Radio,Battery {
    switchRadio() {}
    checkBatteryStauts() {}
}

上例中,CellPhone 实现了 RadioBattery 接口,既能开关收音机,也能检测电量。

接口相互继承

接口与接口之间可以是继承关系,即将两个接口的方法整合:

interface Radio {
    switchRadio(flag: boolean): void
}

interface RadioWithBattery extends Radio{
    checkBatteryStauts();
}

class CellPhone  implements RadioWithBattery {
    switchRadio() {}
    checkBatteryStauts() {}
}

上例,RadioWithBattery继承了Radio接口,除了拥有checkBatteryStauts方法之外,还拥有一个新方法 switchRadio

接口继承类

常见的面向对象语言中,接口是不能继承类的,但是在 TypeScript 中却是可以的:

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

interface Point3d extends Point {
    z: number;
}

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

在TypeScript中当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 Point 的类型(实例的类型)。

所以我们既可以将 Point 当做一个类来用(使用 new Point 创建它的实例):

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

const p = new Point(1, 2);

也可以将 Point 当做一个类型来用(使用 : Point 表示参数的类型):

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

function printPoint(p: Point) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

这个例子实际上可以等价于:

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

interface PointInstanceType {
    x: number;
    y: number;
}

function printPoint(p: PointInstanceType) {
    console.log(p.x, p.y);
}

printPoint(new Point(1, 2));

所以回到 Point3d 的例子中,我们就能很容易的理解为什么 TypeScript 会支持接口继承类了:

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

interface PointInstanceType {
    x: number;
    y: number;
}

// 等价于 interface Point3d extends PointInstanceType
interface Point3d extends Point {
    z: number;
}

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

当我们声明 interface Point3d extends Point 时,Point3d 继承的实际上是类 Point 的实例的类型。

换句话说,可以理解为定义了一个接口 Point3d 继承另一个接口 PointInstanceType。

所以「接口继承类」和「接口继承接口」没有什么本质的区别。

值得注意的是,PointInstanceType 相比于 Point,缺少了 constructor 方法,这是因为声明 Point类时创建的Point类型是不包含构造函数的。另外,除了构造函数是不包含的,静态属性或静态方法也是不包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法)

即声明 Point 类时创建的 Point 类型只包含其中的实例属性和实例方法:

class Point {
    /** 静态属性,坐标系原点 */
    static origin = new Point(0, 0);
    /** 静态方法,计算与原点距离 */
    static distanceToOrigin(p: Point) {
        return Math.sqrt(p.x * p.x + p.y * p.y);
    }
    /** 实例属性,x 轴的值 */
    x: number;
    /** 实例属性,y 轴的值 */
    y: number;
    /** 构造函数 */
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    /** 实例方法,打印此点 */
    printPoint() {
        console.log(this.x, this.y);
    }
}

interface PointInstanceType {
    x: number;
    y: number;
    printPoint(): void;
}

let p1: Point;
let p2: PointInstanceType;

上例中最后的类型 Point 和类型 PointInstanceType 是等价的。 同样的,在接口继承类的时候,也只会继承它的实例属性和实例方法。






泛型

泛型的出现

我们先来看如下示例

在上述例子中,定义了一个echo函数,该函数作用是将传入的参数返回,上述例子中echo函数传入的参数为数值,而返回了一个any,那么我们的变量就丧失了类型,所以泛型就是为了解决变量的指定类型。

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

现在我们将上述例子引入泛型:

function echo<T>(arg :T): T {
    return arg
}
const str: string = 'str'
const num: number = 123

const result1 = echo(str)
const result2 = echo(num)

上例中,我们在函数名后添加了<T>,其中T用来指代任意输入的类型,也规定后面的参数为T类型 arg :T,函数返回值也为T类型可使用了。

在这里的泛型有点类似类型的占位符,等到实际使用时在规定类型 T

上述例子中result1的类型为string,result2的类型为number,都与传入参数的类型一一对应。解决了变量丧失类型问题。

多个类型参数

定义泛型的时候,可以一次定义多个类型参数

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

const result = swap(['string', 123]); // const result: [number, string]

上述例子中,result的返回值类型为result: [number, string],这就是泛型的作用。理所当然result[0] 可以调用number类型的所有方法,当然result[1] 可以调用string类型的所有方法

泛型约束

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);    // (Error)类型“T”上不存在属性“length”。ts(2339)
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了.

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface IWithLength {
    length: number
}

function echoWithLength<T extends IWithLength>(arg: T): T {
    console.log(arg.length);
    return arg;
}

const str = echoWithLength('str')
const obj = echoWithLength({ length: 10})
const arr = echoWithLength([1, 2, 3])

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 IWithLength 的形状,也就是必须包含 length 属性。String Object Array 都有length 属性, 所以str obj arr调用echoWithLength也不会报错。

泛型类

class Queue {
    private data = [];
    push(item) {
        return this.data.push(item)
    }
    pop() {
        return this.data.shift()
    }
}

const queue = new Queue()
queue.push(1)
queue.push('str')
console.log(queue.pop().toFixed())
console.log(queue.pop().toFixed())

上例编译时会报错,因为数组中的string类型调用了number类型的方法toFixed(),所以该Queue类需要改为泛型类。

class Queue<T> {
    private data = [];
    push(item: T) {
        return this.data.push(item)
    }
    pop(): T {
        return this.data.shift()
    }
}

const queue = new Queue<number>()
queue.push(1)
console.log(queue.pop().toFixed())

const queue2 = new Queue<string>()
queue2.push('str')
console.log(queue2.pop().length)

泛型接口

可以使用含有泛型的接口来定义函数的形状:

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

注意,此时在使用泛型接口的时候,需要定义泛型的类型

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}





类型别名

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

type PlusType = (x: number, y: number) => number

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

const sum2: PlusType  = sum

上例中,我们使用 type 创建类型别名。

类型别名常用于联合类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

上述例子中,getname函数的参数允许两种类型string与函数类型(返回值为`string),

创建一个类型别名 Namestring 类型

所以创建一个类型别名NameResolver 类型是函数类型返回值为string

再创建一个联合类型NameOrResolver 方便读写






类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。 (就是告诉编译器当前值的类型以确定。)

类型断言的用途

类型断言的常见用途有以下几种:

  • 将一个联合类型断言为其中一个类型
    当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性或方法,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型特有的属性或方法,所以需要使用类型断言比如:
function getLength(input: string | number): number {
    const str = input as String
    if (str.length) {
        return str.length
    } else {
        const number = input as Number
        return number.toString().length
    }
}

这样就可以在不确定类型的时候访问其中一个特有的方法。

上述案例还有一种简单的写法

function getLength(input: string | number): number {
    if((<string>input).length) {
        return (<string>input).length
    } else {
        return input.toString().length
    }
}

注意:类型断言不是类型转换,如果断言成联合类型之外的类型,则会报错

  • 将一个父类断言为更加具体的子类

  • 将任何一个类型断言为 any

  • 将 any 断言为一个具体的类型






声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

什么是声明语句

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。

我们通常这样获取一个 idfoo 的元素:

$('#foo');
// or
jQuery('#foo');

但是在 ts 中,编译器并不知道 $jQuery 是什么东西:

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型2

declare var jQuery: (selector: string) => any;

jQuery('#foo');

上例中,declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

什么是声明文件

通常我们会把声明语句放到一个单独的文件(jQuery.d.ts)中,这就是声明文件

// src/jQuery.d.ts

declare var jQuery: (selector: string) => any;
// src/index.ts

jQuery('#foo');

声明文件必需以 .d.ts 为后缀。

一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。所以当我们将 jQuery.d.ts 放到项目中时,其他所有 *.ts 文件就都可以获得 jQuery 的类型定义了。

假如仍然无法解析,那么可以检查下 tsconfig.json 中的 filesincludeexclude 配置,确保其包含了 jQuery.d.ts 文件。

第三方声明文件

当然,jQuery 的声明文件不需要我们定义了,社区已经帮我们定义好了:jQuery in DefinitelyTyped

我们可以直接下载下来使用,但是更推荐的是使用 @types 统一管理第三方库的声明文件。 @types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:

npm install @types/jquery --save-dev

可以在这个页面搜索你需要的声明文件。