TypeScript 知识汇总(二)(3W 字长文)

2,238 阅读30分钟

文章使用的 TypeScript 版本为3.9.x,后续会根据 TypeScript 官方的更新继续添加内容,如果有的地方不一样可能是版本报错的问题,注意对应版本修改即可。

前言

该文章是笔者在学习 TypeScript 的笔记总结,期间寻求了许多资源,包括 TypeScript 的官方文档等多方面内容,由于技术原因,可能有很多总结错误或者不到位的地方,还请诸位及时指正,我会在第一时间作出修改。

文章中许多部分的展示顺序并不是按照教程顺序,只是对于同一类型的内容进行了分类处理,许多特性可能会提前使用,如果遇到不懂的地方可以先看后面内容。

下面内容接 TypeScript 知识汇总(一)(3W 字长文)

4.TypeScript 中的函数

4.1 函数声明

  • 函数声明法

    function fun1(): string {
      //指定返回类型
      console.log('string')
      return 'string'
    }
    fun1()
    
    function fun2(): void {
      console.log('void')
    }
    fun2()
    
  • 函数表达式

    let fun = function (): number {
      //其实只对一般进行了校验
      console.log(123)
      return 123
    }
    /*
    	完整写法:在后面细讲规则
    	let fun:()=>number = function():number{
             console.log(123);
             return 123;
    	}
    */
    
    fun()
    
  • 匿名函数

    let result: number = (function (): number {
      return 10
    })()
    console.log(result) //10
    

注意:

  • TypeScript 中的函数也是可以做类型校验的,不过在返回返回值时与箭头函数返回返回值类似

    //(str:string)=>number 就是对于函数的类型校验,是函数类型Function的深度校验的写法
    let fun: (str: string) => number = function (str: string): number {
      console.log(str)
      return 123
    }
    fun('string')
    
    //返回一个具有返回值的函数
    let fun: (str: string) => () => number = function (): number {
      return () => {
        return 123
      }
    }
    /*
        但是一般都只写一边,因为TypeScript有根据上下文的类型推断机制,可以写更少的代码
    */
    
  • TypeScript 中的函数即使没有返回值也应该显示声明返回值为void类型,否则为any类型

    const a = function():void = {
        console.log("function")
    }
    

4.2 函数传参

在 TypeScript 中对函数传参,形参也需要指定对应类型,如果实参传入类型错误会报错,但是形参也可以指定多种数据类型,也可以是类

function fun(name: string, age: number): string {
  return `${name}----${age}`
}
console.log(fun('张三', 18))
function fun(name: string, age: number | string): string {
  return `${name}----${age}`
}
console.log(fun('张三', '18'))

4.2.1 可选参数

在 JS 中函数的形参和实参可以不一样,但是在 TypeScript 中必须要一一对应,如果不一样就必须要配置可选参数

注意: 可选参数必须配置到参数的最后面,可以有多个可选参数

//通过给最后的参数加上?来设置可选参数
function fun(name: string, age?: number): string {
  if (age) {
    return `${name}----${age}`
  } else {
    return `${name}----年龄保密`
  }
}
console.log(fun('张三', 18)) //张三----18
console.log(fun('李四')) //李四----年龄保密

4.2.2 默认参数

在 TypeScript 中默认参数的传入和 JS 中一样,如果没有传入该参数那么默认就会使用默认参数

注意: 默认参数和可选参数不能在同一个形参变量上使用,默认参数可以不用写在参数的最后,但是如果不是写在最后而是写在前面的参数上又想要使用默认参数,可以给可选参数的位置传入undefined,这样函数就会使用默认参数

//直接给形参赋值
function fun(name: string = '王五', age?: number): string {
  if (age) {
    return `${name}----${age}`
  } else {
    return `${name}----年龄保密`
  }
}
console.log(fun('张三', 18)) //张三----18
console.log(fun()) //王五----年龄保密
//直接给形参赋值
function fun(name: string = '王五', age: number): string {
  return `${name}----${age}`
}
console.log(fun(undefined, 18)) //王五----18

4.2.3 剩余参数

在 TypeScript 中的剩余参数也是和 JS 中的一样,通过扩展运算符(...)接受剩余参数

注: 剩余参数可以看做是多个可选参数组成的数组

//通过扩展运算符传入参数
function sum(a: number, b: number, ...result: number[]): number {
  let sum: number = a + b
  sum = result.reduce(function (prev: number, next: number): number {
    return prev + next
  })
  return sum
}

console.log(sum(0, 1, 1, 2, 3, 4, 5)) //16

4.3 函数重载

在 TypeScript 中通过为同一个函数提供多个函数类型定义来实现多种功能的目的

注:

  • 在 JS 中,如果出现了同名方法,在下面的方法会替换掉上面的方法
  • 在 TypeScript 中的函数重载不同于JavaC++这种,而是要在最后一个函数(该函数被叫做函数实体)中通过判断类型来做到
//函数重载
function fun(name: string): string
function fun(age: number): string
function fun(str: any): any {
  //如果要进行函数重载判断,那么这个形参和返回值的类型必须要包含上面函数
  //根据传入参数的不同进入不同的重载函数中,虽然这里的类型为any,但主要是为了包含上面,传参由上面判断
  if (typeof str === 'string') {
    return '我是' + str
  } else {
    return '我的年龄为' + str
  }
}
//fun函数能传入的参数只能是string和number,传入其他参数会报错
console.log(fun('张三')) //我是张三
console.log(fun(18)) //我的年龄为18
console.log(fun(true)) //错误写法,报错
//函数重载
function fun(name: string): number //返回不同的类型
function fun(name: string, age: number): string
function fun(name: any, age?: any): any {
  //也可以传入可选参数,根据参数的不同进入不同的重载函数
  if (age) {
    return '我是' + name + ',年龄为' + age
  } else {
    return 123
  }
}

console.log(fun('张三', 18)) //我是张三,年龄为18
console.log(fun('张三')) //123
console.log(fun('张三', true)) //错误写法,在重载函数中无法找到对应函数

4.4 箭头函数

在 TypeScript 中箭头也是和 JS 中的一样的用法

setTimeout((): void => {
  console.log(123)
})

4.5 this

TypeScript 中的 this 是可以在函数传参数时手动指定其类型限定,这个类型限定可以为 TypeScript 提供推断依据

class Animal {
  name: string = '动物'
  /* 
	一般来说可以直接指定this的指向,在以前的版本如果不知道TypeScript是不知道this应该有什么属性的,this显示为any类型,只有在编译时才会报错(在新版中已经可以不用对this进行类型指向了,默认是当前的函数所指向的对象)
	*/
  eat(this: Animal, food: string) {
    // 注意this不会占据参数的位置,这个函数实际只有一个参数,只用传入一个参数
    console.log(this.name)
    console.log(food)
    console.log(this.age)
    // TypeScript会报错,因为Anmial的实例没有age属性
  }
}

let a = new Animal()
a.eat('food')
class Animal {
  name: string = '动物'
  eat(this: void) {
    //而如果指定的void再调用下面的则会报错,因为类型不匹配了
    console.log(this.name)
  }
}

let a = new Animal()
a.eat()

5.TypeScript 中的类

5.1 类的定义

TypeScript 中的类和 JS 中类的定义基本一样,只是做了额外的类型检验

5.1.1 实例类类型

class Person {
  name: string //和JAVA类似,要先在这声明对象中拥有的属性和类型
  /*该属性定义相当于public name:string;,只不过省略了public,下面再做解释*/
  constructor(n: string) {
    this.name = n //和ES6一样,传入构造函数,只不过需要在前面先定义this的属性
  }

  run(): void {
    console.log(this.name)
  }
}
/*
	类也可以写成表达式写法:
	const Person = class {
	}
*/

let p: Person = new Person('张三') //可以对实例类型变量进行类型的定义,也可以默认不写为any
p.age = 18 //报错,类只允许有name属性
p.run() //张三

注意:

  • constructor 函数后面不能加上返回值的修辞符,否则会报错,可以看作是固定写法
  • 函数定义完成后不需要加上符号分割

5.1.2 静态类类型

上面直接把类作为类型的修辞符只是用作将该类型限定为实例的类类型,如果要限定为类本身需要使用typeof

class Person {
  static max: number = 100
  name: string
  constructor(n: string) {
    this.name = n
  }

  run(): void {
    console.log(this.name)
  }
}

let Person2: typeof Person = Person //把Person类赋值Person2
// 当然这种赋值实际上是把Person对象给了Person2
console.log(Person === Person2) // true

//typeof Person是表明该对象是一个Person类的类型,而不是Person的实例类型
Person2.max = 150
// 可以直接在类上修改原有的属性,这样是修改静态属性
Person2.min = 0 // 报错,因为Person类没有min静态属性
let p2: Person = new Person2('李四') //因为Person2也是Person类型的类,所以可以这样实例对象
p2.run()
console.log(Person2.max) // 150

5.2 类的继承

类的继承和接口不同,一个类只能继承一个父类

class Person {
  name: string
  constructor(n: string) {
    this.name = n
  }
  run(): void {
    console.log(this.name)
  }
}

class Student extends Person {
  //类的继承可以说和ES6完全一样,只是constructor需要指定类型
  age: number
  constructor(name: string, age: string) {
    super(name)
    this.age = age
  }
  work() {
    console.log(this, age)
  }
}
let s = new Student('李四', 18)
s.run()
s.work() //18

注意: 如果子类里的方法和父类方法名一致,那么在使用的时候会使用子类的方法,不会使用父类的方法了

class Person {
  name: string
  constructor(n: string) {
    this.name = n
  }
  run(): void {
    console.log(this.name)
  }
}

class Student extends Person {
  constructor(name: string) {
    super(name)
  }
  run() {
    super.run() //李四,super可以在子类中代表父类
    console.log(this.name + '子类方法')
  }
}
let s = new Student('李四')
s.run() //李四子类方法

5.3 类的修辞符

在 TypeScript 中定义属性或方法的时候为我们提供了四种修辞符

  • public: 公有类型,在类、子类、类外部都可以对其访问

    注: 这里的在类外部访问就是在实例化过后能在类的外界单独打印该属性,而不是只在内部的方法中使用该属性

  • protected: 保护类型,在类、子类里可以对其进行访问,但是在类的外部无法进行访问

  • private: 私有类型,在类里面可以访问,在子类和类的外部都无法访问,在 JS 中要使用私有属性一般只有用_属性/方法模块外部定义内部使用Symbol定义属性的方法来使用,而在 TypeScript 中更加简便

    • TypeScript 使用的是结构性类型系统,当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的
    • 当我们比较带有 privateprotected成员的类型的时候,如果其中一个类型里包含一个private成员,那么只有当另外一个类型中也存在这样一个 private成员,并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。对于 protected成员也使用这个规则
    class Animal {
      private name: string
      constructor(theName: string) {
        this.name = theName
      }
    }
    
    class Rhino extends Animal {
      constructor() {
        super('Rhino')
      }
    }
    
    class Employee {
      private name: string
      constructor(theName: string) {
        this.name = theName
      }
    }
    
    let animal = new Animal('Goat')
    let rhino = new Rhino()
    let employee = new Employee('Bob')
    
    animal = rhino //能够赋值,因为两者兼容,都是同一个私有属性name
    animal = employee // 错误: Animal 与 Employee 不兼容,name的私有属性不兼容
    
  • readonly: 只读类型,可以使用 readonly关键字将属性设置为只读的, 只读属性必须在声明时或构造函数里被初始化。同时readonly修辞符是可以和其他三个修辞符一起存在的,注意readonly必须要放在第二个位置,只写readonly默认在前面加了public

    class Octopus {
      readonly name: string
      readonly numberOfLegs: number = 8
      constructor(theName: string) {
        this.name = theName
      }
    }
    
    let dad = new Octopus('Man with the 8 strong legs')
    dad.name = 'Man with the 3-piece suit' // 错误! name 是只读的.
    

注意:

  • 如果属性不添加修饰符,默认为公有属性(public)

    //public
    class Person {
      public name: string
      constructor(n: string) {
        this.name = n
      }
      public run(): void {
        console.log(this.name)
      }
    }
    
    class Student extends Person {
      constructor(name: string) {
        super(name)
      }
    }
    let s = new Student('李四')
    console.log(s.name) //李四
    s.run() //李四
    
    //protected
    class Person {
      protected name: string
      constructor(n: string) {
        this.name = n
      }
      public run(): void {
        //如果这个方法是protected下面的s.sun()也会报错
        console.log(this.name)
      }
    }
    
    class Student extends Person {
      constructor(name: string) {
        super(name)
      }
    }
    let s = new Student('李四')
    console.log(s.name) //报错
    s.run() //李四
    
  • 如果构造函数也可以被标记成 protected, 意味着这个类不能在包含它的类外被实例化,但是能被继承

    class Person {
      protected name: string
      protected constructor(theName: string) {
        this.name = theName
      }
    }
    // Employee 能够继承 Person
    class Employee extends Person {
      private department: string
    
      constructor(name: string, department: string) {
        super(name)
        this.department = department
      }
    
      public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`
      }
    }
    let howard = new Employee('Howard', 'Sales')
    let john = new Person('John') // 错误:因为'Person' 的构造函数是被保护的.
    
    //private
    class Person {
      private name: string
      constructor(n: string) {
        this.name = n
      }
      run(): void {
        console.log(this.name)
      }
    }
    
    class Student extends Person {
      constructor(name: string) {
        super(name)
      }
      work(): void {
        console.log(this.name)
      }
    }
    let s = new Student('李四')
    console.log(s.name) //报错
    s.work() //报错
    s.run() //李四,因为run方法是Person内部的,可以使用私有属性
    
  • 在子类中通过super调用父类原型的属性和方法时也只能够访问到父类的publicprotected方法,否则会报错

5.3.1 参数属性

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

总的来说,这种写法是上面先声明又赋值属性的简便写法,可以直接通过这种写法改写上方先先在前面声明属性的写法,构造函数中也可以什么都不写

  • 声明了一个构造函数参数及其类型
  • 声明了一个同名的公共属性
  • 当我们 new 出该类的一个实例时,把该属性初始化为相应的参数值
class Octopus {
  readonly numberOfLegs: number = 8
  constructor(readonly name: string) {
    //通过这种写法改变上面对应readonly的例子
  }
}

5.3.2 可选属性

与函数的可选参数一样,在类中也可以定义类的可选属性

class Person {
  name?: string
  constructor(n?: string) {
    this.name = n
  }
  run(): void {
    console.log(this.name)
  }
}
/* 等同下面的写法 */
class Person {
  name: string | undefined
  constructor(n?: string) {
    this.name = n
  }
  run(): void {
    console.log(this.name)
  }
}

5.4 寄存器

TypeScript 中也可以对一个属性时用 get 和 set 方法对一个属性内部的获取和赋值进行拦截

let passcode = 'secret passcode'

class Employee {
  private _fullName: string
  get fullName(): string {
    //对fullName属性进行拦截
    return this._fullName
  }
  set fullName(newName: string) {
    if (passcode && passcode == 'secret passcode') {
      this._fullName = newName
    } else {
      console.log('Error: Unauthorized update of employee!')
    }
  }
}

let employee = new Employee()
employee.fullName = 'Bob Smith'
if (employee.fullName) {
  alert(employee.fullName)
}

注意:只带有 get不带有 set的存取器自动被推断为readonly类型的属性

5.5 静态方法和属性

class Person {
  public name: string
  constructor(n: string) {
    this.name = n
  }
  run(): void {
    console.log(this.name)
  }
}

class Student extends Person {
  static name1: string //设置静态属性
  constructor(name: string) {
    super(name)
    Student.name1 = this.name //赋值
  }
  static work(): void {
    //静态方法
    console.log(Student.name1)
  }
  work(): void {
    console.log(Student.name1)
  }
}
let s = new Student('李四')
console.log(Student.name1) //李四
Student.work()
s.work() //李四

5.6 抽象类

TypeScript 中的抽象类是提供其他类继承的基类,不能直接被实例化,只能被其他类所继承

abstract关键字定义抽象类和抽象类中的抽象方法或属性,抽象类中的抽象方法不包含具体实现,但是必须要在派生类,也就是继承的类中实现抽象方法,抽象属性不需要赋值.并且继承的类不能够扩展自己的方法和属性

总的来说,抽象类和抽象方法只是用来定义一个标准,而在其子类在必须要实现这个标准,并且不能扩展抽象类中没有的标准,否则会报错

注意:abstract声明的抽象方法只能放在抽象类中,否则会报错

abstract class Department {
  abstract age: number
  constructor(public name: string) {
    //参数属性的一个应用
    this.name = name
  }

  printName(): void {
    console.log('Department name: ' + this.name)
  }

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

class AccountingDepartment extends Department {
  public age: number = 18
  constructor() {
    super('Accounting and Auditing') // 在派生类的构造函数中必须调用 super()
  }
  printMeeting(): void {
    console.log('The Accounting Department meets each Monday at 10am.')
  }
  generateReports(): void {
    console.log('Generating accounting reports...')
  }
}

let department: Department // 允许创建一个对抽象类型的引用
department = new Department() // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment() // 允许对一个抽象子类进行实例化和赋值
console.log(department.age) //18
department.printName()
department.printMeeting()
/*
错误: 方法在声明的抽象类中不存在(因为department是抽象类型,如果是直接写的AccountingDepartment类型是不会报错的)
*/
department.generateReports()

6.TypeScript 中的接口

接口是在面向对象编程中一种规范的定义,它定义了行为和动作的规范,起一种限制的作用,只限制传入到接口的数据

TypeScript 中的接口类似于 JAVA,同时还增加了更灵活的接口类型,包括属性、函数、可索引和类等

注意: 不要把接口看做是一个对象字面量,而更像是一个代码块,在其中每个人属性或方法的限制可以用逗号、分号甚至是直接用换行(不写分号逗号,但是必须要隔行书写)隔开,如果写在一行就必须用逗号或分号隔开

6.1 属性类型接口

  • 属性类接口一般用作对于 json 对象的约束(下面的代码还没有使用接口)

    //ts定义方法中传入参数就是一种接口
    function print1(str: string): void {
      console.log(str) //约束只能且必须传入一个字符串参数
    }
    print1('string')
    
    /*
    对json对象进行约束,这是用了带有调用签名的对象字面量,其实仔细一看就像是匿名接口
    */
    function print2(obj: { name: string; age: number }): void {
      console.log(obj) //约束只能传有带有name和age属性的对象
    }
    print2({ name: '张三', age: 18 })
    
    function print3(obj: { name: string; age: 18 }): void {
      console.log(obj) //约束只能传有带有name和age属性的对象,并且age必须为18
    }
    print3('张三', 19) //报错
    print3('张三', 18)
    
  • 对批量方法进行约束:使用接口

    通过interface关键词对接口进行定义

    interface FullName {
      firstName: string //注意这里要;
      secondName: string
    }
    /*
    	加入一个用法
    	let a: FullName['firstName'];//显示a为string类型,因为接口中的值可以单独获取来得到类型
    */
    
    function printName(name: FullName): void {
      console.log(name.firstName, name.secondName)
    }
    let obj = {
      //属性的位置可以不一样
      firstName: '张',
      secondName: '三'
    }
    printName(obj) //传入对象必须有firstName和secondName
    
    let obj2 = {
      firstName: '李',
      secondName: '四',
      age: 18
    }
    function printInfo(info: FullName): void {
      //使用接口可以对批量的函数进行约束,并且内部职能
      console.log(info.firstName + info.secondName + info.age)
    }
    // 使用这种方式TypeScript不会进行类型检验
    printInfo(obj2) //原则上只能传入只含有firstName和secondName的对象,但是如果写在外面传入也不会报错
    /*
    	但是上面这种方法在传入参数的时候不会报错,但是在函数内部使用info.age的时候就会报错,因为接口内部没有设置age属性,如果不想报错,函数内部使用形参的属性必须是只能有接口定义的
    */
    printInfo({
      firstName: '李',
      secondName: '四',
      age: 18
    }) //通过这种方式传入就会直接报错
    

    可选属性接口: 和函数的传参一样,可以在接口处用?代表可选接口属性

    interface FullName {
      firstName: string //注意这里要;结束
      secondName?: string
    }
    function printName(name: FullName): void {
      console.log(name.firstName, name.secondName) //张 undefined
    }
    
    printName({
      firstName: '张'
    })
    

    案例: 利用 TS 封装 ajax 请求

    interface Config {
      type: string
      url: string
      data?: string
      dataType?: string
    }
    
    function ajax(config: Config) {
      let xhr: XMLHttpRequest = new XMLHttpRequest()
      xhr.open(config.type, config.url, true)
      xhr.send(config.data)
    
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 20) {
          if (config.dataType.toLowerCase() === 'json') {
            console.log(JSON.parse(xhr.responseText))
          } else {
            console.log(xhr.responseText)
          }
        }
      }
    }
    //由于接口的要求,必须传入type和url
    ajax({
      type: 'get',
      data: 'name=张三',
      url: 'http://www.baidu.com/',
      dataType: 'json'
    })
    

6.2 函数类型接口

函数类型接口用于对方法传入的参数和返回值进行约束,可通过接口进行批量约束

//加密的函数类型接口
interface encrypt {
  (key: string, value: string): string
  a: string
}

let md5: encrypt = function (key: string, value: string): string {
  //函数必须是两个参数,并且类型对应接口,同时返回值必须是接口的返回值string
  return key + '---' + value
}

console.log(md5('name', '张三'))

注: 函数类接口和类类接口类型区别在于函数类接口不用写函数名,只需要现在后面的参数返回值等,而类类型接口需要限制方法名和属性等

6.3 可索引类型接口

可索引接口通常用作对数组和对象进行约束(但是这个接口不常用)

//对数组使用
interface Arr {
  [index: number]: string //定义索引必须为number,值为string,否则会报错
}
let arr: Arr = ['123', '456']
/*
	其实该接口的用法同数组指定类型的定义
	let arr:number[]=[1,2,3]
	let arr:Array<string>=["123","456"]
*/
//对对象使用,想要约束对象的属性值时可以使用
interface Obj {
  [index: string]: string //定义索引必须为string,值为string,否则会报错
}
let obj: Obj = { name: '张三', age: '20' } //age不能是number
//可索引接口也可以用来对一个属性接口进行额外对象的属性检验,可以用这种方式来跳过属性检查
interface Arr {
  //该接口可以限制一个对象必须有color和width,还可以有其他的属性
  color: string
  width: number
  [propName: string]: any
}

注意: 在同时使用stringnumber类型的可索引接口时,number 类型的索引对应的值必须是 string 类型的子类型或同级类型, 否则会报类型出错误

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

class Dog extends Animal {
  breed: string
  constructor(n: string, b: string) {
    super(n)
    this.breed = b
  }
}

class Cat extends Animal {
  breed: string
  constructor(n: string, b: string) {
    super(n)
    this.breed = b
  }
}

class Cat extends Animal {
  breed: string
  constructor(n: string, b: string) {
    super(n)
    this.breed = b
  }
}

interface NotOkay {
  [x: number]: Animal // 在这会报错,数字索引类型“Animal”不能赋给字符串索引类型“Dog”
  [x: string]: Dog
}
/*
	实际上两者都存在数字索引最后是被转换为了string类型的,比如传入1实际上时'1',相当于将Animal类型的值转换为了Dog,而Dog是Animal类型的子类型,当然不能够父类型转换为子类型,而如果数字索引为其他的string、number等基础类型一样会报错,因为不是Dog的子类型
*/
// 下面两种都不会报错
interface NotOkay {
  [x: number]: Cat // 这里不会报错是因为Cat拥有Dog相同的方法
  [x: string]: Dog
}
/*
interface NotOkay {
  [x: number]: Bird // 报错,没有breed方法
  [x: string]: Dog
}
*/

interface NotOkay {
  [x: number]: Dog
  [x: string]: Animal
}

6.4 类类型接口

6.4.1 实例类接口

实例类类型接口主要用于对类的约束,和抽象类相似

注意: 使用实例类接口只会对实例的属性进行限制,不过对类的静态属性进行限制(包括构造器函数 constructor,即使写了想对应的限制属性也不会起到作用,要限制需要使用构造器类接口)

interface Animal {
  //其实这个说是属性类型也没错.因为eat也可以说是一个属性
  name: string
  eat(str: string): void //这个接口也可以用作对生成的类的实例化对象的检验
}

class Dog implements Animal {
  //类类型接口通过这种写法限制类
  constructor(public name: string) {} //类中必须有name属性和eat方法
  eat(str: string) {
    console.log(this.name + '吃' + str)
  }
}
let dog: Dog = new Dog('狗')
dog.eat('狗粮')

class Cat implements Animal {
  private age: number = 2 //也可以有其他的属性,这点和抽象类相同
  constructor(public name: string) {}
  eat() {
    /*
	  如果接口要字符串类型的参数,这里可以不传参可以传字符串类型的参数,如果接口要求不传参,这里就不能传		  参,否则报错
   */
    console.log(this.name + '吃鱼')
    return 123 //接口为void或者any或者number时可以返回number,否则会报错,其余类型对应
  }
  public showAge() {
    //也可以有其他的方法
    console.log(this.age)
  }
}

let cat: Cat = new Cat('猫')
console.log(cat.eat()) //123
cat.showAge() //2

6.4.2 构造器与静态类接口

实例类接口类型主要是对于类返回的实例进行限制,而构造器类接口就是对类使用new时来对构造器函数进行限制

interface AnimalBehavior {
  eat(str: string): void
}
// 限定一个类有一个构造器接收name与age同时返回的实例对象符合AnimalBehavior接口
interface Animal {
  new (name: string, age: number): AnimalBehavior
  a: string // a就是一个静态的属性,也就是函数上的属性
}
// 这里的ctor必须有constructor方法并且返回一个AnimalBehavior实例且还有一个静态的a属性
function createAnimal(ctor: Animal, name: string, age: number): AnimalBehavior {
  // 这边的return其实已经是由最后返回值得AnimalBehavior来进行限制的,new所做的工作已经结束了
  return new ctor(name, age)
}

class Dog implements AnimalBehavior {
  constructor(name: string, age: number) {}
  static a = 'A' // 必须要有这个静态的属性,否则下面的createAnimal函数会报错
  eat(str: string) {
    console.log('eat ' + str)
  }
}

let d = createAnimal(Dog, 'dog', 2)
d.eat('meat')

6.5 混合类型接口

混合类型接口是讲多种类型接口混合从而合成一个集成多种条件限制的接口

//如将函数类型与属性类型混用,创建一个含有属性的函数
interface Counter {
  (start: number): number
  interval: number
  reset(): void
}

function getCounter(): Counter {
  let counter: Counter = function (start: number): number {
    return start++
  } as Counter //必须进行断言,将这个函数当做一个Couter类型,否则会报错
  counter.interval = 123

  counter.reset = function () {
    this.interval = 0
  }
  return counter
}

let c = getCounter()
//这个混合类型限制的变量本身是个函数,但是有reset方法和interval属性
c(10)
c.reset()
console.log(c.interval)
c.interval = 5
console.log(c.interval)

6.6 接口扩展

接口扩展与类的继承类似,可以用子接口扩展父接口,从而拿到多个接口的限制条件

interface Animal {
  eat(): void
}

interface Person extends Animal {
  //继承父接口的限制条件
  name: string
  work(): void
}

class Student implements Person {
  //接口会同时将前面两者的接口限制合并
  constructor(public name: string) {}
  eat() {
    console.log(this.name + '吃饭')
  }
  work() {
    console.log(this.name + '上学')
  }
}

let stu: Student = new Student('小明')
stu.eat()
stu.work()
//接口和继承相结合
interface Animal {
  eat(): void
}
interface Plant {
  wait(): void
}
//也可以继承多个接口,用逗号隔开
interface Person extends Animal, Plant {
  name: string
  work(): void
}

class YoungPerson {
  constructor(public name: string) {}
  drink() {
    console.log(this.name + '喝水')
  }
}
//混合继承和接口限制的类
class Student extends YoungPerson implements Person {
  constructor(name: string) {
    super(name)
  }
  eat() {
    console.log(this.name + '吃饭')
  }
  work() {
    console.log(this.name + '上学')
  }
  wait() {
    console.log(this.name + '停下')
  }
}

let stu: Student = new Student('小明')
stu.eat()
stu.drink()
stu.work()
stu.wait()

6.7 继承类类型接口

TypeScript 允许类也可以当作接口来使用,所以也可以被接口所继承

class Control {
  private state: any
}

//继承类的接口可以继承到一个类的私有和包含属性,接口会检验一个类是否继承有该父类的这两个属性
interface SelectableControl extends Control {
  select(): void
}
//一个继承Control类的Button类,虽然state是private类型不能再内部调用.但是确实继承了这个属性,不报错
class Button extends Control implements SelectableControl {
  select(): void {}
}
//只继承了Control类,内部可以定义其他方法
class Radio extends Control {
  select(): void {}
}
//这个类会报错,因为没有继承Control类,没有state属性
class Input implements SelectableControl {
  select(): void {}
}
//即使写了private的state也会报错,因为state是在上一个类中是私有的,不能在外部访问,两个state是不同的
class Input2 implements SelectableControl {
  private state = 123
  select(): void {}
}
/*
	如果上面的Control类型是public,那么在Input2中的state只要是设置为public类型就不会报错,设置为其	  他类型会和接口不符合,则会报错
*/

7.TypeScript 中的泛型

泛型就是解决类、接口等方法的复用性问题,以及对不特定数据的支持问题的类型

如: 我们想通过传入不同类型的值而返回对应类似的值,在 TypeScript 中可以通过 any 类型的返回值解决返回值的不同,但是不能解决规定同一个函数传入指定不同类型参数的问题,而且用 any 作为返回类型性能没有泛型高,并且不符合规范

7.1 泛型函数

可以使用 TypeScript 中的泛型来支持函数传入不特定的数据类型,要求传入的参数和返回的参数一致

function fun<T>(value: T): T {
  //一般用T代表泛型,当然也可以是其他的非关键字和保留字,可以在函数内用
  let data: T //T就代表着泛型函数要使用的泛型,通过后期的传入来使用
  data = value
  return data
}

console.log(fun<boolean>(true))
console.log(fun(123))
/*
如果不传泛型参数会利用类型推论自动推导出来,这里或推断出来是number类型,如果没有指定泛型类型的泛型参数,会把所有泛型参数当成any类型比较
*/

注意: 如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入 T 的类型,在一些复杂的情况下,这是可能出现的。在大部分情况下,都是通过泛型的自动推断来约束用户的参数是否正确

7.2 泛型类

通过泛型类可以实现对类内部不同类型变量的分别管理

//如:有个最小堆算法,需要同时支持返回数字和字符串两种类型,可以通过类的泛型来实现
class MinNum<T> {
  public list: T[] = []
  add(value: T): void {
    this.list.push(value)
  }
  min(): T {
    let minNum = this.list[0]
    for (let i in this.list) {
      if (minNum > this.list[i]) {
        minNum = this.list[i]
      }
    }
    return minNum
  }
}
let min1 = new MinNum<number>() //通过泛型实现类不同变量类型的内部算法,比any类型效率更高
min1.add(1)
min1.add(2)
min1.add(996)
min1.add(7)

console.log(min1.min()) //1

let min2 = new MinNum<string>()
min2.add('a')
min2.add('c')
min2.add('e')
console.log(min2.min()) //a

7.2.1 把类当做参数的泛型类

//将类当做传参的约束条件,只允许指定的类的实例作为参数传入
class Person {
  name: string | undefined
  //这里如果没有写或者为undefined会报错,因为TypeScript怕定义了却不赋值,除非在construct中进行了赋值
  age: number | undefined
}

class Student {
  show(info: Person): boolean {
    //参数只允许传入Person类的对象
    console.log(info)
    return true
  }
}

let per = new Person()
per.name = '张三'
per.age = 18
let stu = new Student()

stu.show(per)
//使用泛型类可以手动的对不同种类的条件进行约束
//将类当做传参的约束条件,只允许指定的类的实例作为参数传入
class Person {
  name: string | undefined
  age: number | undefined
}

class User {
  userName: string | undefined
  password: string | undefined
}

class Student<T> {
  show(info: T): void {
    //参数只允许传入Person类的对象
    console.log(info)
  }
}

let per = new Person()
per.name = '张三'
per.age = 18
let stu = new Student<Person>() //T在这传入的是泛型类,作为show方法的校验
stu.show(per)

let user = new User()
user.password = '123456'
user.userName = '张三'

let stu2 = new Student<User>() //可以写入不同的类
stu2.show(user)

案例

/*
功能:定义一个操作数据库的库,支持Mysql,Mysql,MongoDb
要求:Mysql、Mssql、MongoDb功能一样,都有add、updata、delete、get方法
注意:约束统一的规范、以及代码重用
解决方案:需要约束规范所以要定义接口,需要代码重用所以用泛型
*/
interface DBI<T> {
  add(info: T): boolean
  update(info: T, id: number): boolean
  delete(info: T): boolean
  get(id: number): any[]
}

//定义一个操作mysql数据库的类
//注意:要实现泛型接口,这个类应该是个泛型类
class MysqlDb<T> implements DBI<T> {
  add(info: T): boolean {
    console.log(info)
    return true
  }
  update(info: T, id: number): boolean {
    throw new Error('Method not implemented.')
  }
  delete(info: T): boolean {
    throw new Error('Method not implemented.')
  }
  get(id: number): any[] {
    return [
      {
        title: 'xxx',
        desc: 'xxxxx',
        id: id
      },
      {
        title: 'xxx',
        desc: 'xxxxx',
        id: id
      }
    ]
  }
}

//定义一个操作mssql数据库的类
class MssqlDb<T> implements DBI<T> {
  add(info: T): boolean {
    throw new Error('Method not implemented.')
  }
  update(info: T, id: number): boolean {
    throw new Error('Method not implemented.')
  }
  delete(info: T): boolean {
    throw new Error('Method not implemented.')
  }
  get(id: number): any[] {
    throw new Error('Method not implemented.')
  }
}

//操作用户表,定义一个User类和数据表做映射
class User {
  username: string | undefined
  password: string | undefined
}

let u = new User()
u.username = '张三'
u.password = '123456'

let oMysql = new MysqlDb<User>() //类作为约束条件

oMysql.add(u)
console.log(oMysql.get(10))

7.2.2 在泛型里使用构造器类类型

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

// create函数的参数是一个Class,返回值是这个Class的实例
function create<T>(c: { new (): T }): T {
  return new c()
}

注:c:T的意思是,c 的类型是 T,但这个函数的目的不是要求 c 的类型是 T,而是要求 c 就是 T

// 下面这种作比较
let num = new Number(1)
fn(Number)
fn(num)

更高级的简写的用法来使用原型属性推断并约束构造函数与类实例的关系

class BeeKeeper {
  hasMask: boolean
}

class ZooKeeper {
  nametag: string
}

class Animal {
  numLegs: number
}

class Bee extends Animal {
  keeper: BeeKeeper
}

class Lion extends Animal {
  keeper: ZooKeeper
}

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

createInstance(Lion).keeper.nametag // typechecks!
createInstance(Bee).keeper.hasMask // typechecks!

解析:

  • c:{new():T}里的new是构造函数的方法,意思是这个 c 是一个有着构造函数方法的对象,下面的return new c();里的new是创建一个新的实例的new 二者是不同的东西

  • c:new()=>Tc:{new():T}是一样的,前者是后者的简写,意即 c 的类型是对象类型且这个对象包含返回类型是 T 的构造函数

    注意: 这里的=>不是箭头函数,只是用来标明函数返回类型

7.3 泛型接口

通过对接口使用泛型,通过对函数和类接口的使用来自己实现对于传入参数调节的限制

//因为类和函数接口差距不大,所以这里就只写函数类泛型接口
//第一种写法
interface encrypt {
  <T>(value: T): T
}

let md5: encrypt = function <T>(value: T): T {
  //通过泛型函数赋值
  return value
}

console.log(md5<string>('张三')) //泛型声明刚好和接口内部的顺序相呼应
console.log(md5<boolean>(true))

//第二种写法
interface encrypt<T> {
  (value: T): T
}
//在将接口给变量的时候就指定类型给
let md5: encrypt<string> = function <T>(value: T): T {
  //通过泛型函数赋值
  return value
}
//在这就可以直接使用函数,而不需要指定泛型
console.log(md5('张三'))

/*
其实两种方法根据对于接口<T>写的位置的不同可以大致推断出其泛型声明指定的位置,一般来说第二种在工业编程中用的是最多的
*/

7.4 泛型限定

因为泛型可以是任意的类型,而如果想要对泛型的类型进行相应的约束时,可以使用使用extends关键字对其进行约束

注意: 这里的extends不再是继承这类意思,而且起到限定与约束作用

function identity<T>(arg: T): T {
  console.log(arg.length) // 这里会报错,因为T是任意类型,所有不一定有length属性
  return arg
}
// 写成这样是不会报错的,因为参数为一个泛型组成的数组
function identity<T>(arg: T[]): T[] {
  console.log(arg.length) // 这里会报错,因为T是任意类型,所有不一定有length属性
  return arg
}
// 我们可以对泛型进行限定来解决报错
interface Lengthwise {
  length: number
}
// 约束传入的参数必须要带有length属性
function identity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}
identity(5) // 报错,5是number类型,不具有length属性
identity('string') // 字符串具有length属性

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

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

// 让K被约束为T的key,keyof是索引类型查询操作符
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') // okay
getProperty(x, 'm') // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

注: 上面的这种写法就不能再调用函数的时候来手动写一下约束条件了,只能让它自动推断出来

更多内容

TypeScript 知识汇总(一)(3W 字长文)

TypeScript 知识汇总(二)(3W 字长文)

TypeScript 知识汇总(三)(3W 字长文)