typescript 笔记(二)

324 阅读13分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

往期文章

typescript 笔记(一)

上一篇文章介绍了 typescript 的基础类型和一些其他的内容,本文就来记录下 ts 的一些常用功能。

类型断言

类型断言,简而言之,就是对某个变量的类型进行说明。

比如var times = 2;这个语句生命的times变量是一个Number类型。

当然有人说我知道啊,代码我自己写的我能不知道吗?其实断言诗给 ts 看的,ts 用断言来帮你判断你的代码逻辑中是否出现了问题,而且别人也能通过代码中的类型断言很快理解这些变量的意思。

总之,类型断言是 ts 非常牛逼的功能,简直是小母牛坐火箭--牛逼上天了~

那么在 typescript,如何使用类型断言呢?

let sth: any = 'this are sth special here'

由于sth是个任意类型,所以它可能存在或者不存在length属性,假如我们想读取length属性,就需要断言它是一个字符串类型。这里有两种方法,其中在 tsx 中必须使用第二种

  • let sthLength: number = (<string>sth).length
  • let sthLength2: number = (sth as string).length

类型推断

针对每一个变量写类型断言,是非常浪费时间的事情,ts 显然也明白这个道理,因此,ts 提供了类型推断这样的功能。

所谓的类型推断,就是我们写代码定义变量的时候不必须明确的指明类型,后续 ts 可以根据变量的值自动推倒该变量的类型,所以类型推断要求定义变量的时候必须进行赋值

let myFavoriteNumber = 'seven';
myFavoriteNumber = 7; //error

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

let myFavoriteNumber;
myFavoriteNumber = 'seven'; //OK
myFavoriteNumber = 7; //ok

联合类型

很多时候,一个变量的类型不一定是固定的,比如我们经常对一个间隔执行进行初始化赋值:

var timer = null
timer = setTimeout(console.log, 1000)

针对这种情况,ts 推出了联合类型的概念,用于表示变量的取值范围可以为多种类型中的一种或者几种的集合。

let myFavoriteNumber: string | number;

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

function getLength(something: string | number): number {
    something.length; //error
    something.toString(); //ok
}

另外,已经定义为联合类型的变量在被赋值的时候,ts 会根据类型推论的规则推断出一个确定的类型

let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); //error

接口

接口(interface) 本质上是对一个事物的描述,在 js 中本身没有面向对象编程的概念,所以通过对对象或者函数进行描述,从而在使用该对象或者函数的层面上形成了约定。

为了和对象函数等普通变量区分,接口一般首字母大写,并且赋值的时候,变量的形状必须和接口的形状保持一致。

所以,接口可以用于三个方面:

  • 用于对类的一部分行为进行抽象,
  • 也常用于对「对象的形状(Shape)」进行描述,如函数,
  • 也可用于二者的混合。
interface Person {
    name: string;
    age: number;
}

interface Sum{
    (x:number,y:number):number
}
let sum:Sum = (a,b)=> { return a+b }

任意属性与可选属性

但是通常说来,一个已经定义好的对象在使用上和定义总是不尽相同,这种情况下,接口定义可以使用可选的属性与任意的属性。

可选的属性用?来表示,任意的属性用[]来表示

interface Person {
    age?: string; //可选的属性
}
interface Animal {
     [propName: string]: string; //任意的属性
}

需要注意:如果定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

interface Person {
    [propName: string]: string; //任意的属性的值类型都为 string
    age?: number;  //error 因为定义了任意属性,且任意属性为 string 类型,所以这里 number 类型是不对的
}

只读属性

有时候我们希望定义的对象中某些属性不可被外界更改,言外之意这个属性是只读的,因此这里需要用到只读属性

只读属性:readonly 可以用来定义某个属性只读,而不能被再次赋值

interface Person {
    readonly id: number;
}

let tom: Person = {
    id: 89757
}

tom.id = 1 //error

接口继承

面相对象的三个基本特征是:继承,封装和多态。

为了更好的支持 js 面向对象编程,ts 规定接口是可以继承的。继承用extends来表示。

加入我们有一个基础类,叫做形状,那么这个形状有个color属性

interface Shape {
  color: string
}

然后我们需要实现一个四边形的接口,就可以继承形状这个基础类,从而获得color这个属性

interface Square extends Shape {
  sideLength: number
}
let square = {} as Square
square.color = 'blue'
square.sideLength = 10

继承是可以一对多继承的,也就是说,一个接口可以继承自多个接口


interface PenStroke extends Shape, Square{
  width: number
}

类数组

类数组和普通的数组不一样,类数组是只具有索引和长度属性的对象,比如document.getElementsByClass的返回值

因此类数组不应该用普通数组的方式来定义,而应该用接口。

ts 中针对类数组都已经定义好了接口,因此我们可以直接使用,如 IArguments, NodeList, HTMLCollection, HTMLElement 等

function sum() {
    let args: IArguments = arguments;
}

var p:HTMLElement = document.createElement('p')

内置对象

所谓内置对象类型,出了几个基础和复杂类型,还有Date、Error等。

在 TypeScript 中将变量定义为内置对象类型,比如我们定义一个错误

let e: Error = new Error('Error occurred');

或者定义一个日期类型

let d: Date = new Date();

或者正则表达式

let r: RegExp = /[a-z]/;

同时,js 还有两个非常重要的内置对象,那就是 DOM 和 BOM,相信所有学过 javascrit 的同学,都对这两个对象印象深刻吧。

DOM 和 BOM 提供的内置对象有:Document、HTMLElement、Event、NodeList 等等~

let body: HTMLElement = document.body;
let allDiv: NodeList = document.querySelectorAll('div');
document.addEventListener('click', function(e: MouseEvent) {
  // Do something
})

函数

函数和其他变量最大的区别在于:一个函数有输入和输出。

要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到。

ts 提供的函数的约束也是分成了两种,一种是函数声明,另一种是函数表达式。

其中,函数声明的写法如下:

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

上面代码中(x: number, y: number) => number这一部分就是对mySum的描述

注意看,上面函数表达式中的=>与 ES6 中的箭头函数是不同的,这里的=> 的左边表示函数的输入的约束,右边表示函数的输出的约束

当然,这样的写法通常对阅读的人造成困扰,所以,ts 提供了另外一种描述方式

使用接口对函数进行约束

我们也可以用接口对函数进行约束,我们在前面也写过这个例子,这样看起来可能比表达式更好理解

interface MySum {
    (x: number, y: number): number;
}

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

可选参数

有些时候,尤其是在一些插件对方法的封装中,有些参数并非是必传参数,如果用户没有传递这样的参数,我们可以在内部对其做默认的赋值

所以 ts 提供了可选参数。

可选参数:在函数中也使用 ? 来描述可选的参数,其中可选的参数要放在必需参数的后面

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

参数默认值

ES6 增加了函数参数默认值的用法(a=1)=>a++

对于这种情况,ts 使用参数默认值来约束。

参数默认值:TypeScript 会将添加了默认值的参数识别为可选参数,这时就不受「可选参数必须接在必需参数后面」的限制了

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

参数解构赋值

同时,ts 针对 ES6 的解构也提供了约束写法

function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);

函数重载

函数重载:函数重载是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。

然而 javascript 中函数不存在重载的概念,后声明的函数变量会覆盖之前的同名变量,所以我们通常情况下通过对参数的判断来实现伪重载

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

上面的代码中,我们重复定义了多次函数reverse,前几次都是函数定义,最后一次是函数实现。

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

接口与函数的混合

我们在前面接口那个小结已经说了接口可以用于类与函数的混合。

这是因为类中既可以定义属性,也可以定义属于类的方法。

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

function getCounter(): Counter {
  let counter = (function (start: number) { }) as Counter
  counter.interval = 123
  counter.reset = function () { }
  return counter
}

let c = getCounter()
c(10)
c.reset()
c.interval = 5

ES6 提供了类的概念,从而更方便的使用面向对象的编程思想。

类可以看多是程序中最基础的单元,比如坦克大战中,坦克可以看作类,子弹可以看作类,墙也可以看作类。

所以类本身可以定义多个属性和方法。针对每个属性和方法,出了本身的类型约束外,还有一些其他的约束,比如属性是公有的还是私有的。

修饰符

类的修饰符有很多种:

修饰符含义备注
public公共属性或方法可以任意访问
private私有属性或方法不可被外部访问,不可继承
protected被保护的属性或方法只能被继承
static静态的属性或方法只存在于类本身
readonly只读的属性或方法必须在声明时或构造函数里被初始化
abstract抽象类或方法抽象类不能实例话,抽象方法必须被子类实现

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

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

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

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

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

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

class Cat extends Animal {
    constructor(name) {
        super(name);
        console.log(this.name); //error
    }
}

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

class Animal {
    public name: string;
    private constructor (name: string) {
        this.name = name;
  }
}
class Cat extends Animal { //error
    constructor (name) {
        super(name);
    }
}

let a = new Animal('Jack');//error

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

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

let a = new Animal('Jack'); //error

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

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

class Cat extends Animal {
    constructor(name) {
        super(name);
        console.log(this.name); //ok
    }
}

static 是ES7中的一个提案,用来定义静态的属性,这些属性存在于类本身上面而不是类的实例上

class Animal {
    static num: number = 42;

    constructor() {
        // ...
    }
}
console.log(Animal.num); // 42

readonly 只读属性,必须在声明时或构造函数里被初始化

class Person {
    readonly age: number
  constructor(public readonly name: string, age: number) {
  	this.name = name
  	this.age = age
  }
}
let a = new Animal('Jack',25);
console.log(a.name); // Jack
a.name = 'Tom'; //error
a.age = 26; //error

abstract 用于定义抽象类和其中的抽象方法,首先,抽象类是不允许被实例化,其次,抽象类中的抽象方法必须被子类实现

abstract class Animal {
    public name: string;
    public constructor(name) {
        this.name = name;
    }
    public abstract sayHi(): void;
}

let a = new Animal('Jack');  //error

class Cat extends Animal {
    public sayHi(): void {    //继承类必须实现这个方法
        console.log(`Meow, My name is ${this.name}`);
    }
    public eat(): void {
        console.log(`${this.name} is eating.`);
    }
}

let cat = new Cat('Tom');

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

// 先检查用户密码是否正确,然后再允许其修改员工信息
class Employee {
  private _fullName: string
  private _passcode: string

  constructor(readonly passcode: string){
    this._passcode = passcode
  }
  get fullName(): string {
    return this._fullName
  }

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

let employee = new Employee('secret passcode')
employee.fullName = 'Bob Smith'
if (employee.fullName) {
  console.log(employee.fullName)
}

接口与类

凡是允许使用接口的地方也允许使用类

class Point {
  x: number
  y: number
}

interface Point3d extends Point {
  z: number
}

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

类实现接口

interface 不仅可以用来对 class 进行属性描述,而且也可以实现 class 的方法,这叫做 类实现接口。

假如不同类之间有一些共有的特性,就可以把特性提取成 interfaces,用 implements 关键字来实现。

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

interface Alarm {
    alert();
}

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

interface Light {
    lightOn();
    lightOff();
}

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

接口继承类

前面已经说过,接口是可以互相继承的,其实接口也可以继承类。

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

interface Point3d extends Point {
    z: number;
}

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