学了两天TypeScript,也该总结总结了

254 阅读23分钟

学了两天TypeScript,也该总结总结了

前言

学了两天ts,最大的一个感受就是:把不确认的事情确定,你就可以掌控后续

在看TypeScript入门教程的时候,感觉入门并没有太大的难度,所以别慌,耐下性子学两天,基本上把这个

[TypeScript入门教程]  ts.xcatliu.com/introductio… 

看完也就会使用any大法了....学了几天了,来做一下总结,温故而知新

TypeScript是什么

简单来说,就是JavaScript加了类型要求,「类型」就是其最核心的特性

  • 你得明确告诉编译器你写的东西是什么类型,而编译器会为你提供该类型的相关的代码提示和纠正错误
  • 如果你没有告诉编译器你写的东西是什么类型,编译器会自动去推断你写东西是什么类型

举个栗子

这段代码在JS编译的时候是不会报错的,只有在运行的时候才会报错

let val = 123
console.log(val.length)

这是一位杠精就说了:“我一看就知道他会出错,数字哪有length属性,谁会这么写哦”

这时候系统就说了:“是的,你了不起,你清高,你知道我不知道啊,我只有运行的时候才知道,你为什么不早点告诉我,让我犯这么低级的错误,上线了才被人耻笑,哭唧唧”

这个时候有一位名为TypeScript的靓仔就出现了,“让我帮你避免掉这些低级错误让你更健壮”,于是乎就出现了这段代码

let val:number = 123
console.log(val.length)

这段代码在你强行写出来的时候就会告诉,类型“number”上不存在属性“length”。就无法编译通过;至此这种低级错误,就得以解决;如果你写的类型没有这些属性或方法,会在编译的时候就告诉你,不会让你上台了,在被人耻笑;

这两段代码没有太大的区别,编译的结果也是一样的,无非就是在使用ts写的时候后面加多了一个:number声明了这个变量的类型,那接着就来了解一下类型;看看我们平时写的变量都应该怎么样去声明类型。

类型

原始数据类型

原始数据类型有:字符串数字布尔值undefinednull、以及ES6的Symbol和ES10的Bigint

字符串
let name: string = '发仔'
数字
let age: number = 26
布尔值
let isBoy: boolean = true
undefined
let height: undefined = undefined
null
let weight: null = null
Symbol
let id: symbol = Symbol('20200013')
Bigint
let boyfriendPower:bigint=BigInt(99999999999999999999999999999999999999999999999999n)

如你所见,原始类型的声明就这么简单

:使用new操作符的构造函数返回的不是原始数据类型哦,如new String,new Number,new Boolean等,这些声明要用对应的内置对象去定义

联合类型

联合类型表示取值可以为多种类型中的一种,联合类型使用 | 分隔每个类型

let myFavoriteNumber: string | number;
myFavoriteNumber = 'nine';
myFavoriteNumber = 9;
联合类型的属性或方法

当我们定义了一个变量为联合类型的时候,我们只能访问该联合类型中所有类型的共同的属性或方式,例如

function getLength(something: string | number): number {
   // return something.length; // length 不是 string 和 number 的共有属性,所以会报错
    return something.toString() // 访问 string 和 number 的共有属性是没问题
}

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

let myFavoriteNumber: string | number | number[];
myFavoriteNumber = 'nine';
console.log(myFavoriteNumber.length); // 4
myFavoriteNumber = 9;
console.log(myFavoriteNumber.length); // 编译时报错,被推断成了 number,没有length属性

任意值

顾名思义,任意值表示变量可以赋值为任意类型,any 类型,访问任何属性、调用任何方法

let anyThing: any = 123456;
anyThing.name = 'George'
console.log(anyThing.length);
anyThing.sayHello();

注:声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值,也就是说any会滋生更多any

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

let something;
// 等价于
let something: any;

使用any大法,其实就和写JS没啥区别了,那为啥还要有any这个类型呢?我想是因为“效率

对象的类型

在ts中,我们用接口来定义对象的类型,接口简单理解就是对 对象形状的描述,接口只负责表述,不负责实现,比如

interface Person {
  name: string,
  age: number,
  isBoy: boolean,
  height: undefined,
  weight: null,
  id: symbol,
  boyfriendPower: bigint
}

可以看到我们简单描述了一个Person对象的形状,但没有具体描述其各个属性的值或方法,仅仅只是形状

定义好接口之后,我们就要用这个接口的形状来约束数据的形状,少一些属性和多一些属性,都是不行的;值的类型也必须和定义的类型一样

let Grorge: Person = {
  name: '发仔',
  age: 26,
  isBoy: true,
  height: undefined,
  weight: null,
  id: Symbol('20200013'),
  boyfriendPower: BigInt(99999999999999999999999999999999999999999999999999n)
}

这时候一位靓仔提出疑问了,那我不希望完全匹配这个形状可以么,是可以的,使用可选属性

可选属性
interface Person {
  name: string,
  age: number,
  isBoy: boolean,
  height?: undefined,
  weight?: null,
  id: symbol,
  boyfriendPower?: bigint
}

如上,只要加了?的属性,就可以不存在啦

let Grorge: Person = {
  name: '发仔',
  age: 26,
  isBoy: true,
  id: Symbol('20200013') 
}

这是另外一位靓仔就说了,我能不能添加没有定义的属性,答案是可以的,只要定义任意属性,即可随便添加

任意属性
interface Person {
  name: string,
  age: number,
  isBoy: boolean,
  height?: undefined,
  weight?: null,
  id: symbol,
  boyfriendPower?: bigint,
  [x:string]:any
}

注:如果定义了任意属性,那么确定属性和可选属性的类型必须是它类型的子集;如上这个any就包含了所有的类型,即任意类型都可以

这个时候就可以随意定义变量,只要属性是string类型

let Grorge: Person = {
  name: '发仔',
  age: 26,
  isBoy: true,
  id: Symbol('20200013'),
  boyfriendPower: BigInt(99999999999999999999999999999999999999999999999999n),
  occupation: '前端开发工程师',
  workingYears: 2,
  isOnJob: true
}

此时,又有靓仔说了,我看你的对象里面又id属性,我不想这个属性被修改,只能读取怎么办?

只读属性

有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性

interface Person {
  name: string,
  age: number,
  isBoy: boolean,
  height?: undefined,
  weight?: null,
  readonly id: symbol,
  boyfriendPower?: bigint,
  [x:string]:any
}

此时的id就不能被修改了

注,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

数组的类型

定义数组中子项的类型要求,有比较多的的表示方法

类型+[]表示法
let fibonacci: number[] = [1, 1, 2, 3, 5];
泛型表示法
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
接口表示法
interface NumberArray {
    [index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

我们一般不会用这个表示法,它常用来表示类数组,如arguments;事实上常用的类数组都有自己的接口定义,如 IArguments, NodeList, HTMLCollection 等, TypeScript 中已经定义好了这些类型供我们使用

函数的类型

一个函数,我们要重点关注的是这个函数的输入类型和输出类型(如果沒有返回值我們用void表示)

// 函数声明式
function sum(x: number, y: number): number {
    return x + y;
}
// 函数表达式
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};
  • 输入多余的(或者少于要求的)参数,是不被允许的

  • 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型

  • 也可以用接口去定义函数的形状

    interface Func {
        (x: number, y: number):number
    }
    

那这时候来了一位靓仔说了,我想输入多少个参数就多少个参数,不可以么?是可以的,有三种方式可以做到?

  • 可选参数
  • 参数默认值
  • 剩余参数
可选参数

与接口中的可选属性类似,我们用 ? 表示可选的参数

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

注:可选参数后面不允许再出现必需参数了

参数默认值

给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数

function buildName(firstName: string = 'Tom', lastName: string) {
  return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');

注:此时参数位置没有限制了

剩余参数
function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}
​
let a = [];
push(a, 1, 2, 3)

注:剩余参数只能是最后一个参数

函数重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'

利用联合类型,我们可以这么实现:

function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

这时,我们可以使用重载定义多个 reverse 的函数类型:

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

类型断言

类型断言就是手动指定一个值的类型,你可以指鹿为马,马到成功,功...

语法
as 类型

<类型>

建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法,这样不会和泛型搞混

类型断言的用途

  • 将一个联合类型断言为其中一个类型
  • 将一个父类断言为更加具体的子类
  • 将任何一个类型断言为 any(any大法好啊)
  • any 断言为一个具体的类型(使我们的代码向着高可维护性的目标发展)
类型断言的限制

那是不是任何一个类型,都能被断言为另一个类型呢,看个例子


interface Animal {
  name: string;
}
interface People {
  name:string;
  age: number;
}
const Animal1 = (animal: Animal) => {
  return (animal as People);
}
const isMySelf = (mySelf: People) => {
  return (mySelf as Animal);
}

这个是可以相互断言的,因为他们都有name属性重叠

interface Animal {
  name: string;
}
interface People {
  age: number;
}
const Animal1 = (animal: Animal) => {
  return (animal as People);
}
const isMySelf = (mySelf: People) => {
  return (mySelf as Animal);
}

这个是不能相互断言的,类型 "Animal" 到类型 "People" 的转换可能是错误的,因为两种类型不能充分重叠

教程说:要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可

关于理解这句话需要去前置去理解一下,结构类型系统和类型兼容性;

我这边简单理解就是要存在重叠部分,则可以相互断言;

双重断言
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型

所以:先断言成any再断言为其他类型, as any as Foo除非迫不得已,千万别用双重断言

类型断言 vs 类型转换

类型断言并不会真的影响到变量的类型,若要进行类型转换,需要直接调用类型转换的方法

function toBoolean(something: any): boolean {
    return something as boolean;
}
toBoolean(1);   // 返回值为 1
​
​
function toBoolean(something: any): boolean {
    return Boolean(something);
}
toBoolean(1); // 返回值为 true
类型断言 vs 类型声明

类型声明是比类型断言更加严格,为了增加代码的质量,我们最好优先使用类型声明,这也比类型断言的 as 语法更加优雅

这个例子使用断言是可以相互断言的,但是使用类型声明是不可以的

interface Animal {
  name: string;
}
interface People {
  name: string;
  age: number;
}
​
let animal: Animal = { name: 'Tom' }
let tom: People  = animal 
// 编译报错 类型 "Animal" 中缺少属性 "age",但类型 "People" 中需要该属性。
​

断言:只需要满足 Animal 兼容 PeoplePeople 兼容 Animal 即可(有重叠属性即可)

赋值:满足 People 兼容 Animal(需要属性一致)

类型别名

用来给一个类型起个新名字,类型别名常用于联合类型,例子

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

字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一

type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
    // do something
}
​
handleEvent(document.getElementById('hello'), 'scroll');  // 没问题
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick'

上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

注意,类型别名与字符串字面量类型都是使用 type 进行定义

元组

数组是相同类型的对象,而元组可以是不同类型的对象

let george: [string, number, boolean] = ['发仔', 26, true];

注:当添加新的元素的时候,它的类型被限制为元组中每个类型的联合类型

george.push(Symbol('20200013'))
//错误:类型“symbol”的参数不能赋给类型“string | number | boolean”的参数

枚举

枚举(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["Sat"] === 6); // true
​
console.log(Days[0] === "Sun"); // true
console.log(Days[6] === "Sat"); // true
手动赋值

未手动赋值的枚举项会接着上一个枚举项递增

enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

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

enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};
​
console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

上面的例子中,递增到 3 的时候与前面的 Sun 的取值重复了,但是 TypeScript 并没有报错,导致 Days[3] 的值先是 "Sun",而后又被 "Wed" 覆盖了

手动赋值的枚举项可以不是数字,此时需要使用类型断言来让 tsc 无视类型检查 (编译出的 js 仍然是可用的):

enum Days {Sun = 7, Mon, Tue, Wed, Thu, Fri, Sat = <any>"S"};

当然,手动赋值的枚举项也可以为小数或负数,此时后续未手动赋值的项的递增步长仍为 1

enum Days {Sun = 7, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};
​
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1.5); // true
console.log(Days["Tue"] === 2.5); // true
console.log(Days["Sat"] === 6.5); // true
常数项和计算所得项

枚举项有两种类型:常数项(constant member)和计算所得项(computed member)。

前面我们所举的例子都是常数项,一个典型的计算所得项的例子:

enum Color {Red, Green, Blue = "blue".length};

上面的例子中,"blue".length 就是一个计算所得项

上面的例子不会报错,但是如果紧接在计算所得项后面的是未手动赋值的项,那么它就会因为无法获得初始值而报错

enum Color {Red = "red".length, Green, Blue};

当满足以下条件时,枚举成员被当作是常数:

  • 不具有初始化函数并且之前的枚举成员是常数。在这种情况下,当前枚举成员的值为上一个枚举成员的值加 1。但第一个枚举元素是个例外。如果它没有初始化方法,那么它的初始值为 0

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

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

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

常数枚举

和常量的定义相似,用const enum 定义常数枚举;常数枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员

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

进阶

在ES6之前,我们是通过构造函数和原型链继承来实现累的概念,在ES6之后,出了类,其实就是语法糖,让我们写对象原型更加清晰、更像面向对象编程的语法

类相关的概念的简单介绍
  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通过 new 生成
  • 面向对象(OOP)的三大特性:封装、继承、多态
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 CatDog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
  • 存取器(getter & setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
ES6 +中类的用法
属性和方法

使用 class 定义类,使用 constructor 定义构造函数。

通过 new 生成新实例的时候,会自动调用构造函数。

class Animal {
    public name;
    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

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

class Animal {
  name = 'Jack';
  constructor() {
    // ...
  }
}
​
let a = new Animal();
console.log(a.name); // Jack
继承

使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。

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
存取器

使用 getter 和 setter 可以改变属性的赋值和读取行为:

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 num = 42;  
  static isAnimal(a) {
    return a instanceof Animal;
  }
}
​
let a = new Animal('Jack');
Animal.isAnimal(a); // true
console.log(Animal.num); // 42
a.isAnimal(a); // TypeError: a.isAnimal is not a function
a.num; // undefined
TypeScript 中类的用法
访问修饰符

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

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

下面举一些例子:

class Animal {
  public firstName: string
  private secondName: string
  protected thirdName: string
  public constructor(firstName: string, secondName: string, thirdName: string) {
    this.firstName = firstName
    this.secondName = secondName
    this.thirdName = thirdName
  }
}
​
let a = new Animal('杰克', '小老鼠', '大哥');
console.log(a.firstName); // 杰克
a.firstName = '汤姆';
console.log(a.firstName); // 汤姆console.log(a.secondName); //属性“secondName”为私有属性,只能在类“Animal”中访问。console.log(a.thirdName); //属性“thirdName”受保护,只能在类“Animal”及其子类中访问。
  • 当构造函数修饰为 private 时,该类不允许被继承或者实例化;使用 private 修饰的属性或方法,属性是无法直接存取,在子类中也是不允许访问的
  • 当构造函数修饰为 protected 时,该类只允许被继承;如果是用 protected 修饰,则允许在子类中访问
class Mouse extends Animal {
  constructor(firstName: string, secondName: string, thirdName: string) {
    super(firstName, secondName, thirdName);
    console.log(this.firstName); // 杰克
    console.log(this.secondName); // 属性“secondName”为私有属性,只能在类“Animal”中访问。
    console.log(this.thirdName); // 大哥
  }
}
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 和其他访问修饰符同时存在的话,需要写在其后面。

抽象类

abstract 用于定义抽象类和其中的抽象方法,

  • 抽象类不允许被实例化
  • 抽象类中的抽象方法必须被子类实现
abstract class Animal {
  public name: string;
  public constructor(name: string) {
    this.name = name;
  }
  public abstract sayHi(): void;
}
​
class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}
​
let cat = new Cat('Tom');

类与接口

前面接口说到了,接口可用于于对「对象的形状」进行描述,这一段结算接口另外一个用途,对类的一部分行为进行抽象

类实现接口

不同类之间可能有一些共性,这时候就可以把这些共性提取成接口(interfaces),用 implements 关键字来实现

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

interface Alarm {
    alert(): void;
}
​
class Door {
}
​
class SecurityDoor extends Door implements Alarm {
    alert() {
        console.log('SecurityDoor alert');
    }
}
​
class Car implements Alarm {
    alert() {
        console.log('Car alert');
    }
}

一个类可以实现多个接口:

interface Alarm {
    alert(): void;
}
​
interface Light {
    lightOn(): void;
    lightOff(): void;
}
​
class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

上例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯。

接口继承接口

接口与接口之间可以是继承关系:

interface Alarm {
    alert(): void;
}
​
interface LightableAlarm extends Alarm {
    lightOn(): void;
    lightOff(): void;
}

这很好理解,LightableAlarm 继承了 Alarm,除了拥有 alert 方法之外,还拥有两个新方法 lightOnlightOff

接口继承类

常见的面向对象语言中,接口是不能继承类的,但是在 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};

因为:当我们在声明 class Point 时,除了会创建一个名为 Point 的类之外,同时也创建了一个名为 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;
}

类型 Point 和类型 PointInstanceType 是等价的

泛型

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

一个类型参数

先看这个例子,以创建一个指定长度的数组,同时将每一项都填充一个默认值

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
​
createArray(3, 'x'); // ['x', 'x', 'x']

这段代码不会编译报错,但是他有一个显而易见的缺陷是,它并没有准确的定义返回值的类型:Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型

这时候,泛型就派上用场了:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}
​
createArray<string>(3, 'x'); // ['x', 'x', 'x']
// 相当于 function createArrayGenerics<string>(length: number, value: string): string[]
createArrayGenerics(3, 123456) // ['123456', '123456', '123456']
// 相当于 function createArrayGenerics<number>(length: number, value: number): number[]
createArrayGenerics(5, true) // [true, true, true]
// 相当于 function createArrayGenerics<boolean>(length: number, value: boolean): boolean[]

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来

多个类型参数

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

// 多个类型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}
​
swap<string, number>(['george', 25])
// 相当于 function swap<string, number>(tuple: [string, number]): [number, string]
​
swap<string, boolean>(['tom', true])
// 相当于 function swap<string, boolean>(tuple: [string, boolean]): [boolean, string]
泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法

function loggingIdentity<T>(arg: T) {
  // arg.length   //类型“T”上不存在属性“length}

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

interface Lengthwish {
  length: number
}
​
function loggingIdentity<T extends Lengthwish>(arg: T): number {
  return arg.length
}

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了

多个类型参数之间也可以互相约束

// 多个类型参数之间相互约束
function copyFields<T extends U, U>(target: T, source: U): T {
  for (const key in source) {
    target[key] = (<T>source)[key]
  }
  return target
}
​
copyFields({ a: 1, b: 2, c: 3, d: 4 }, { b: 10, d: 20 }) // =>{ a: 1, b: 10, c: 3, d: 20 }
// function copyFields<{  a: number;  b: number;  c: number;  d: number;}, {  b: number;  d: number;}>(target: {  a: number;  b: number;  c: number;  d: number;}, source: {  b: number;  d: number;}): {  a: number;  b: number;  c: number;  d: number;} (+1 overload)
泛型接口

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

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

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

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}
​
let createArray: CreateArrayFunc;
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']

进一步,我们可以把泛型参数提前到接口名上:

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

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

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
​
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型参数默认类型
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;
}

使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用

声明文件

至此,基础的语法就学习的差不多了,声明文件主要在使用第三方类库框架的时候需要应用到,这样才能获得对应的代码补全、接口提示等功能;一般常用的第三方类库框架都有声明文件,只要对应去下载就可以使用了;

主要语法
  • declare va 声明全局变量
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interfacetype 声明全局类型
  • export导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global扩展全局变量
  • declare module 扩展模块
  • /// <reference /> 三斜线指令
typescript声明文件搜索

可以再这个网址查询

书写声明文件

书写一个声明文件并不是一件简单的事,如有需要可以看教程

最后

学了些typescript之后,我将一个Vue2的项目集成了typescript;主要使用的是vue-class-componentvue-property-decorator,有时间再总结一下,在这里建议,老项目没有必要去集成,当然如果动手能力强,也想写写ts,还是可以去弄的,只要第三方声明文件就够喝一壶了;使用Vue3的话,那肯定就是要用typescript了

参考文章-TypeScript入门教程