TypeScript 学习(下篇)

116 阅读7分钟

类型别名

用于给一个类型起一个新名字,常用于联合类型

type FirstName = string;
type LastName = () => string;
type Name = FirstName | LastName;

字符串字面量类型

用来约束变量的取值,使其只能是某几个字符串中的一个

type Name = 'jack' | 'lilei' | 'anna';
function sayHi(name: Name){
    //do something
}

sayHi('jack') //true
sayHi('小明') //error

元组

数组中放置的是相同类型的变量或者对象,而元组中放的都是不同类型的对象,例如

let x: [string, number] = ['aa', 11] //设定了一个第一项为字符串,第二项为数字的数组

x[0] = 'aa'
x[1] = 11
  • 元组可以接受赋值
let x: [string, number]
x[0] = 'hello'  //true
  • 但是要注意的是直接对元组进行初始化时,需要提供元组内所有类型指定的项
let tom: [string, number];
tom = ['Tom'];  //false
tom = ['Tom', 1] //true
  • 给元组添加原来组内不存在的元素时,元素的类型会被限制为元组内原有类型的联合类型
let person: [string, number]
person = ['jack', 18]
person.push('你好') //true
person.push(true)  //error

枚举

语法

枚举使用 enum 关键字来进行定义

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

默认赋值

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

Days["Sun"] === 0  //true
Days["Mon"] === 1  //true 
Days["Tue"] === 2  //true
Days["Sat"] === 6  //true
Days[0] === "Sun"  //true 
Days[1] === "Mon"  //true 
Days[2] === "Tue"  //true 
Days[6] === "Sat"  //true 

此时,上方代码实际上最后会被编译为

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 Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

Days["Sun"] === 7  // true
Days["Mon"] === 1  // true
Days["Tue"] === 2  // true
Days["Sat"] === 6  // true

未赋值的枚举项会被自动赋值为上一项的值 +1,然后往后递增

  • 注意,typescript 不会发现手动赋值和默认赋值之间的冲突,因此要注意避免
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

Days["Sun"] === 3  // true
Days["Wed"] === 3  // true
Days[3] === "Sun"  // false
Days[3] === "Wed"  // false
  • 如果手动复制的值是小数或者负数,后续的项依旧是递增+1
enum Days {Sun = 3, Mon = 1.5, Tue, Wed, Thu, Fri, Sat};

Days["Tue"] === 2.5 //true

常数项和计算所得项

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

此时,计算所得项 Color["Blue"] = 4

  • 但是要注意的是,计算所得项的后面不能跟未手动赋值的项,否则会报错。也就是说,计算所得项必须是枚举类型中的最后一个值
enum Color {Green, Blue = "blue".length, Red}; //error

常数枚举

就是使用 const enum 定义的枚举类型

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

假如包含了计算所得项,那么就会报错

const enum Directions {Up, Down, Left, Right = 'right'.length}   //error

外部枚举

就是使用 declare enum 定义的枚举类型

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

可以同时使用 declareconst 进行声明

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

假如包含计算所得项,也会报错

declare enum Directions {Up, Down, Left, Right = 'right'.length} //error

  • 类(Class):定义了一件事物的抽象特点,包括他的一些属性和方法
  • 对象(Object):类的实例化,通过 new 关键字来生成
  • 面向对象的三大特性:继承封装多态

ES6 中的 class

使用 constructor 来定义构造函数

class Person{
    public name
    constructor(name){
        this.name = name
    }
    sayHi(){
        return `Hello, my name is ${this.name}`
    }
}

let demo = new Person('jack')
console.log(demo.sayHi()) //Hello, my name is jack

类的继承

使用 extends 关键字来实现继承,同时,子类中使用 super 关键字来调用父类中的构造函数

class Boy extends Person{
    constructor(name){
        super(name)  //调用父类的 constructor(name) 方法
        console.log(this.name)
    }
    sayHi(){
        return super.sayHi() + ', i am a boy'
    }
}

let LiMing = new Boy('LiMing')  //LiMing
console.log(LiMing.sayHi())  //Hello, my name is LiMing, i am a boy

存取器

使用 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 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

TypeScript 中类的用法

public

代表公有属性,可以在任何地方访问到,所有属性和方法默认都是 public 的

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

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

let a = new Animal('cat'); // true

private

代表私有属性,不能在声明类以外的地方访问,在子类中也不能访问到

class Animal {
  public name;
  private constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {  //error, Cannot extend a class 'Animal'. Class constructor is marked as private.
  constructor(name) {
    super(name);
  }
}

let a = new Animal('Jack'); //error, Constructor of class 'Animal' is private and only accessible within the class declaration.

protected

受保护的属性,类似于 private,但是 protected 可以在它的子类中访问到

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

let a = new Animal('Jack'); //error, Constructor of class 'Animal' is protected and only accessible within the class declaration

readyonly 属性

只读属性,只允许出现在类型、属性声明中。但是要注意的是,readonly 需要放置在其他修饰符的后方

class Animal {
  public readonly name;
  public constructor(name) {
    // something
  }
}

抽象类

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

什么是抽象类?

首先,抽象类是不允许实例化的,也就是不允许使用 new 关键词来进行实例化

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

let a = new Animal('Jack'); //error, Cannot create an instance of an abstract class.

其次,抽象类中的抽象方法必须被子类实现,如果没实现,就会出现报错

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

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
}

let cat = new Cat('Tom');  //error, Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

需要声明 sayHi()

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`);
  }
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`);
  }
}

类的类型定义

与接口类似

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHi(): string {
    return `My name is ${this.name}`;
  }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

类与接口

类继承接口

实现(implements)是面向对象中的一个重要的概念。因为一个类只能继承另一个类,而这时如果不同类之间有一些共有的特性,这时候就可以把这些特性提取出来,用 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');
    }
}

同时,一个类可以实现(implements)多个接口

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

可以理解为,类 class 是一种对象,继承类相当于被继承类的进化体,而接口 interface 中就是不同类中共有的“行为”部分,比如古代人和现代人都会吃饭,而实现这种“行为”的方法就是通过 implements

接口继承接口

接口与接口之间也可以继承 extends

interface Person {
    eat(): void;
}

interface Man extends Person{
    sayHi(): void;
    walk(): void;
}

接口继承类

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

interface NewDemo extends Demo {
    z: number;
}

let demo: NewDemo = {x: 1, y: 2, z: 3};
  • 为什么别的面向对象语言不支持接口继承类,TypeScript 支持呢?
  • 实际上,我们再声明 class Demo 时,除了会创建一个 Demo 类之外,同时也创建了一个 Demo 的类型(Type),所以,Demo 既可以当一个类来使用(new Demo),也可以当作一个类型来使用(let demo: Demo)
class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

let x: Point = {x:1, y:2}
  • 所以,接口继承类 和 接口继承接口,并没有什么本质上的区别
  • 注意的是,class 类定义时,会有一个 constructor 构造函数,而如果是接口 interface 定义的,则是没有 constructor 的部分的

泛型

泛型指的是,在定义函数、接口、类的时候,不预先去指定具体的类型,而是在使用的时候再去指定类型的行为。

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> 返回的是一个由 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']
  • 其中,我们用 T 来代替原来的 any,这样就可以确保 value 和 Array 中的每一项的类型都是相同的了
  • 在调用函数 createArray(3, 'x') 时,输入的 'x' 会让其自动推算出其类型是 string

多个类型参数

可以同时定义多个类型参数

function Demo<A, B>(array: [A, B]): [B, A] {
    return [array[1], array[0]];
}

Demo([7, 'seven']); // ['seven', 7]

泛型约束

在函数内使用泛型变量时,由于不能确定其正确的类型,所以不能随意操作它的属性和方法

function demo<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}
//error, 泛型变量 arg 并不一定包含属性 length

这时,可以通过泛型约束的方法,只允许该函数传入包含有 length 的变量,这就是泛型约束

interface Base{
    length: number;
}

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

demo('demo')//true
demo(1) //error, Argument of type 'number' is not assignable to parameter of type 'Base'.

此时泛型 T,继承了 Base 接口,也就约束了 T 必须去包含一个 length 的属性,如果传入的参数不包含 length 属性,就会报错。

同时,多个泛型之间也可以互相约束

function copy<A extends B, B>(target: A, source: B): T {
    //do Something
}

此时,A 是继承于 B 的,这样可以确保不会出现 B 有的字段,而 A 没有的情况

泛型接口

interface Demo {
    <T>(length: number, value: T): Array<T>;
}
//或者
interface Demo<T> {
    (length: number, value: T): Array<T>;
}

let demo: Demo;

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

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

泛型类

class Demo<T> {
    value: T;
    add: (x: T, y: T) => T;
}

let myDemo = new Demo<number>();

泛型参数的默认类型

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

声明合并

如果定义了两个名字相同的函数、接口、类,那么他们会自动合并成一个类型

函数的合并(重载)

function demo(x: number): number;
function demo(x: string): string;

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

接口的合并

接口中的属性在合并时会简单的合并到一个接口中

interface Hello{
    hi: string
}
interface Hello{
    sayHi: () => void
}

那也就相当于

interface Hello{
    hi: string,
    sayHi: () => void
}
  • 但是要注意的是,要进行合并的属性,必须是相同的类型
interface Hello{
    hi: string
}
interface Hello{
    hi: string    //true
    sayHi: () => void
}
interface Hello{
    hi: string
}
interface Hello{
    hi: number   //error,类型不一致
    sayHi: () => void
}

接口中的函数方法的合并与普通函数的合并是相同的

interface Hello{
    hi: string,
    sayHi(name: string): void
}
interface Hello{
    hi: string,
    sayHi(name: string, age: number): void
}

也就相当于

interface Hello{
    hi: string,
    sayHi(name: string): void
    sayHi(name: string, age: number): void
}