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

5,308 阅读28分钟

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

前言

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

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

1.什么是 TypeScript?

TypeScript 的首个版本发行于 2012 年 10 月,后续在不断的更新中逐渐在前端领域站稳了脚跟,如今绝大多数的框架都或多或少使用 Typescript 来进行开发。

TypeScript 的主要特点:

  • TypeScript 是 JavaScript 的超集,遵循了最新的 ES6 和 ES5 规范,并且扩展了 JavaScript 的语法,引入了诸如接口、泛型、重构等功功能。
  • TypeScript 有着强类型的约束,代表着 TypeScript 有着极高的代码的可读性和可维护性,比 JavaScript 更加适合开发大型企业项目。
  • TypeScript 能够直接编译成 JavaScript ,编译后的 JavaScript 可以运行到任何符合版本的浏览器上,并且现如今的流行框架都可以继承 TypeScript。

2.TypeScript 的安装及编译

2.1 安装

可以通过下面的方式在全局安装 TypeScript

npm install -g typescript
# 或 yarn add typescript -g

2.2 编译

  • 安装 TypeScript 后通过内置的命令 tsc 就能将 ts 文件编译为对应的 js 文件

    tsc helloworld.ts
    
  • 上面的方法太麻烦,可以在编译器(这里只说在 vscode 中的配置)中自动监视

    • 在项目目录中运行tsc --init生成 tsconfig.json
    • 可以修改 tsconfig.json 中的outDir选项为"outDir": "./js",以后所有的 js 代码都会在这个文件夹中编译
    • 然后在 vscode 中打开命令面板( Ctrl/Command + Shift + P)选择输入task,选择Run Task,监听 tsconfig.json,就可以 vscode 中实时监控 TypeScript 编译
  • 在工程化项目中可以使用 webpack 等打包工具来进行编译,这里不过多说明了

3.TypeScript 的数据类型

TypeScipt 中为了使编写的代码更加规范,更有利于维护,增加了类型校验,所有以后写 ts 代码必须要指定类型

注: 在 TypeScript 后面可以不指定类型,但是后期不能改变其类型,不然会报错,但是只会报错,不会阻止代码编译,因为 JS 是可以允许的

3.1 基本类型

  • 布尔类型(boolean)

    let flag: boolean = true
    //let flag=true 也是可以的
    flag = 123 //错误,会报错,因为不符合类型
    flag = false //正确写法,boolean类型只能写true和false
    console.log(flag)
    
  • 数字类型(number)

    let num: number = 123
    console.log(num)
    
  • 字符串类型(string)

    let str: string = 'string'
    console.log(str)
    
  • 数组类型(Array)

    在 TypeScript 中有四种定义数组的方法

    //第一种定义数组方法
    let arr1:number[]=[1,2,3];//一个全是数字的数组
    
    //第二种定义数组方法
    let arr2:Array<number>=[1,2,3];
    
    //第三种定义数组方法(用了下面的元组类型)
    let arr3:[number,string]=[123,"string"];
    
    //第四种定义数组方法(用了下面的任意类型)
    let arr4:any[]=[1,"string",true,null];
    
  • 只读数组类型(ReadonlyArray)

    只读数组中的数组成员和数组本身的长度等属性都不能够修改,并且也不能赋值给原赋值的数组

    let a: number[] = [1, 2, 3, 4]
    let ro: ReadonlyArray<number> = a //其实只读数组只是一个内置定义的泛型接口
    ro[1] = 5 //报错
    ro.length = 5 //报错
    a = ro //报错,因为ro的类型为Readonly,已经改变了类型
    a = ro as number[] //正确,不能通过上面的方法复制,但是可以通过类型断言的方式,类型断言见下面
    

    注: TypeScript 3.4 引入了一种新语法,该语法用于对数组类型ReadonlyArray使用新的readonly修饰符

    function foo(arr: readonly string[]) {
      arr.slice() // okay
      arr.push('hello!') // error!
    }
    
  • 元组类型(tuple)

    元组类型属于数组的一种,上面一种数组里面只能有一种类型,否则会报错,而元组类型内部可以有多种类型

    //元组类型可以为数组中的每个成员指定一个对应的类型
    let arr: [number, string] = [123, 'string'] //注意如果类型不对应也会报错
    arr[2] = 456 //报错
    //因为元组类型在声明时是一一对应的,只能有2个成员
    

    注意: 与数组一样,元组也可以使用readonly修辞了,但是,尽管出现了readonly类型修饰符,但类型修饰符只能用于数组类型和元组类型的语法

    let err1: readonly Set<number> // error!
    let err2: readonly Array<boolean> // error!
    
    let okay: readonly boolean[] // works fine
    
  • 枚举类型(enum)

    /*
    	通过:
        enum 枚举名{
        	标识符=整形常数,
        	标识符=整形常数,
        	......
        	标识符=整形常数
        };
        定义一个枚举类型
    */
    enum Color {
      red = 1,
      blue = 3,
      oragne = 5
    }
    
    let color: Color = Color.red //color为Color枚举类型,值为Color中的red,也就是1
    console.log(color) //1
    

    注意:

    • 如果没有给标识符赋值,那么标识符的值默认为索引值

    • 如果在期间给某个标识符进行了赋值,而之后的标识符没有赋值,那么之后表示符的索引依次为前面的值加 1

    • 枚举可以引用内部的标识符的值或者外部变量的值

      const o = 5
      enum Color {
        red = 1,
        blue = red, // 引用内部标识符
        oragne = o // 引用外部变量
      }
      
    • 可以把标识符用引号括起来,效果不受影响

    • 还可以通过反过来选择索引值来获取字符串标识符(实际上就是将枚举的变量赋值为一个对象,该对象分别有键对应属性和属性对应键两种相对应的属性)

    enum Color {
      red,
      blue = 3,
      'oragne'
    }
    let color1: Color = Color.red
    let color2: Color = Color.blue
    let color3: Color = Color.oragne
    let color4: string = Color[0] //通过获取索引得到标识符
    console.log(color1) //0
    console.log(color2) //3
    console.log(color3) //4
    console.log(color4) //red
    
  • 任意类型(any)

    任意类型的数据就像 JS 一样,可以随意改变其类型

    let num: any = 123
    num = 'string'
    console.log(num) //string
    

    具体用法

    //如果在ts中想要获取DOM节点,就需要使用任意类型
    let oDiv: any = document.getElementsByTagName('div')[0]
    oDiv.style.backgroundColor = 'red'
    //按道理说DOM节点应该是个对象,但是TypeScript中没有对象的基本类型,所以必须使用any类型才不会报错
    
  • unknown 类型

    TypeScript 3.0 引入了新的unknown 类型,它是 any 类型对应的安全类型。就像所有类型都可以被归为 any所有类型也都可以被归为 unknown 这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。unknownany 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。 而在对 any 类型的值执行操作之前,我们不必进行任何检查

    // unknown可以被赋值为任意类型
    let value: unknown
    value = true // OK
    value = 42 // OK
    value = 'Hello World' // OK
    value = [] // OK
    value = {} // OK
    value = Math.random // OK
    value = null // OK
    value = undefined // OK
    value = new TypeError() // OK
    value = Symbol('type') // OK
    
    // 但是 unknown 类型只能被赋值给 any 类型和 unknown 类型本身,如果没有类型细化的话
    let value: unknown
    let value1: unknown = value // OK
    let value2: any = value // OK
    let value3: boolean = value // Error
    let value4: number = value // Error
    let value5: string = value // Error
    let value6: object = value // Error
    let value7: any[] = value // Error
    let value8: Function = value // Error
    

    直观的说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值

    // 当然也不能直接进行操作,必须要进行类型细化
    let value: unknown
    
    value.foo.bar // Error
    value.trim() // Error
    value() // Error
    new value() // Error
    value[0][1] // Error
    

    注:

    • unknown类型也可以和 any 一样通过typeofinstanceof或自定义类型保护来缩小unknown的范围

    • 在联合类型中,unknown 类型会吸收任何类型。这就意味着如果任一组成类型是 unknown,联合类型也会相当于 unknown,只有与any进行联合才会不同

      type UnionType1 = unknown | null // unknown
      type UnionType2 = unknown | undefined // unknown
      type UnionType3 = unknown | string // unknown
      type UnionType4 = unknown | number[] // unknown
      type UnionType5 = unknown | any // any
      
    • 在交叉类型中,任何类型都可以吸收 unknown 类型。这意味着将任何类型与 unknown 相交不会改变结果类型

      type IntersectionType1 = unknown & null // null
      type IntersectionType2 = unknown & undefined // undefined
      type IntersectionType3 = unknown & string // string
      type IntersectionType4 = unknown & number[] // number[]
      type IntersectionType5 = unknown & any // any
      
    • never类型是unknown类型的子类型

      type t = never extends unknown ? true : false // true
      
    • keyof unknown等于类型never

      type t = keyof unknown // never
      
    • 只能对unknown类型进行等于或非等于的运算符操作,不能进行其他的操作

      let value: unknown
      let value2: number = 1
      console.log(value === value2)
      console.log(value !== value2)
      
    • 使用映射类型时如果遍历的是unknown类型,不会映射任何属性

      type Types<T> = {
        [P in keyof T]: number
      }
      type t = Types<unknown> // t = {}
      
  • undefined 和 null 类型

    虽然为变量指定了类型,但是如果不赋值的话默认该变量还是 undefined 类型,如果没有指定 undefined 直接使用该变量的话会报错,只有自己指定的是 undefined 类型才不会报错

    let flag1: undefined
    /*
    	也可以let flag1:undefined=undefined;
    */
    console.log(flag) //undefined
    
    let flag2: null = null //如果不指定值为null那么打印的时候也会报错
    console.log(flag2) //null
    

    为变量指定多种可能的类型

    let flag: number | undefined //这种写法就不会在没有赋值的时候报错了,因为设置了可能为undefined
    console.log(flag) //undefined
    flag = 123
    console.log(flag) //123 也可以改为数值类型
    
    //也可以设定多个类型
    let flag1: number | string | null | undefined
    flag1 = 123
    console.log(flag1) //123
    flag1 = null
    console.log(flag1) //null
    flag1 = 'string'
    console.log(flag1) //string
    
  • void 类型

    TypeScript 中的 void 表示没有任何类型,一般用于定义方法的时候没有返回值,虽然也能给变量指定类型,但是 void 类型只能被赋值 undefined 和 null,没有什么实际意义

    注: 在 TypeScript 中函数的类型表示其返回值的类型,函数类型为 void 表示其没有返回值

    function run(): void {
      console.log(123)
      //return 不能有返回值,否则报错
    }
    
    function run1(): number {
      console.log(456)
      return 123 //必须有返回值,并且返回值为number类型,否则报错
    }
    
    function run2(): any {
      console.log(789) //因为any是任意类型,所以也可以不要返回值
    }
    
  • never 类型

    never 类型是其他类型(包括 null 和 undefine)的子类型,代表着从来不会出现的值,意味着声明 never 类型的变量只能被 never 类型所赋值

    let a: undefined
    a = undefined
    
    let b: null
    b = null
    
    let c: never //c不能被任何值赋值,包括null和undefiend,指不会出现的值
    c = (() => {
      throw new Error('error!!')
    })() //可以这样赋值
    
    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
      throw new Error(message)
    }
    
    // 推断的返回值类型为never
    function fail() {
      return error('Something failed')
    }
    
    // 返回never的函数必须存在无法达到的终点
    function infiniteLoop(): never {
      while (true) {}
    }
    
  • object 类型

    object表示非原始类型,也就是除numberstringbooleansymbol(TypeScript 中的 Symbol 同 JS 完全一样,在这里是没有讲的),nullundefined之外的类型

    let stu: object = { name: '张三', age: 18 }
    console.log(stu) //{name: "张三", age: 18}
    //也可以
    let stu: {} = { name: '张三', age: 18 }
    console.log(stu)
    
    declare function create(o: object | null): void
    //declare是一个声明的关键字,可以写在声明一个变量时解决命名冲突的问题
    create({ prop: 0 }) // OK
    create(null) // OK
    
    create(42) // 报错
    create('string') // 报错
    create(false) // 报错
    create(undefined) // 报错
    

    注: 一般对象类型不会这样申明,而是直接写让 TypeScript 做自动类型判断或者更加精确的指示,如接口等,在后面都有介绍到

  • 函数类型

    函数类型有多种声明方式,最简单直观的是用Function表示函数类型,在写函数表达式的时候可以直接写声明是一个函数类型(不过一般我们不这样做,要么是不在表达式左边直接写Function而是靠类型推断,要么是直接将一个函数的类型写全),具体介绍在后面函数中单独介绍

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

3.1.1 枚举类型

由于枚举类型的比较特殊,这里单独再拿出来说。在前面已经知道了枚举的基本使用,同时枚举类型相互获取键值的特性也已经清楚,这里说几个特殊情况:

  • 字符串枚举: 枚举的值除了使用整数外还可以是纯的字符串,在这里主要利用其键值相互获取的特性

    enum Message {
      Error = 'error',
      Success = 'success',
      Failed = 'failed'
    }
    // 字符串枚举依然可以使用内部的变量,但不能使用外部的变量
    const f = 'faliled'
    enum Message {
      Error = 'error',
      Success = 'success',
      Failed = Success
      // Failed = f 报错
    }
    
  • 异构枚举: 简单来说就是既包含数字又包含字符串的枚举类型

    // 异构枚举也可以使用内部的变量,但不能使用外部的变量
    enum Message {
      Error = 0,
      Success = 'success',
      Failed = 'failed'
    }
    
  • 枚举作为类型使用: 当满足一定条件时,枚举对象本身和其中的成员都可以当做是类型来使用

    • enum E { A }:无初始值,但是这种类型的成员必须前一个是一个数值类型的枚举成员
    • enum E { A = 'a' }:字符串枚举
    • enum E { A = 1 }:基本的有初始化的枚举类型
    enum Animals {
      Dog = 1,
      Cat = 2
    }
    
    // type的值只能是Animals中的Dog成员,也就是type只能是1
    interface Dog {
      type: Animals.Dog
    }
    const dog: Dog = {
      type: Animals.Dog
     // type: 1
     // 注意这边 type: 1 也可以,这里是所有 Animals 中的值,也就是上面的 1 和 2,所以它也能是Cat,这个就要说到后面的类型兼容了
    }
    
    // 如果直接使用枚举变量,那么相当于高级类型的字符自变量类型
    enum Animals {
      Dog = 1,
      Cat = 2
    }
    
    interface Animal {
      type: Animals // type的值为1或2
    }
    const dog: Dog = {
      type: Animals.Dog // type:1
      // type: Animals.Cat // type:2
    }
    
  • 编译枚举变量: 在正常情况下,枚举不像是type定义的类型这样编译后是不会存在的,枚举类型创建后就是默认开辟了一个枚举变量的空间,并且枚举值我们一般用作提高代码的可读性:

    enum Status {
        Success:200,
    }
    console.log(res.status === Status.Success) // 通常我们会这样为响应对象提高代码可读性
    

    如果不想要枚举变量真实存在,只是想自己创建一个别值,那么可以在枚举变量前加上const关键字

    // 加了const关键字以后相当于原来的枚举变量只是个占位符
    const enum Status {
        Success:200,
    }
    

3.2 高级类型

3.2.1 keyof 与[]

keyof 是索引类型查询操作符。假设 T 是一个类型,那么 keyof T 产生的类型是 T 的属性名称字符串字面量类型构成的联合类型。而[]是索引访问操作符,可以单独使用相当于是对象书写的[]形式,也可以与keyof操作符一起使用

注意: T 是数据类型,并非数据本身

interface Itest {
  webName: string
  age: number
  address: string
}

type ant = keyof Itest // 在编译器中会提示ant的类型是webName、age和address这三个字符串
let a: ant = 'webName' // a只能是三个字符串中的一个

使用[]

interface Type {
  a: string
  b: number
  c: boolean
  d: undefined
  e: null
  f: never
  h: object
}
type Test = Type['a'] // string

如果 T 是一个带有字符串索引签名的类型,那么keyof T是 string 类型,并且T[string]为索引签名的类型

interface Map<T> {
  [key: string]: T
}
let keys: keyof Map<number>
//string|number,因为可索引接口的类型如果是string的话可以传可以为string或者number
/*
interface Map<T> {
  [key: number]: T;
}
let keys: keyof Map<number>; //可索引接口为number则keys的类型只能是number
*/
let value: Map<number>['name'] //number,Map<number>['name']这是一个类型,最后结果是接口里属性的值

keyof操作符也可以配合 type 的类型别名来实现类似字面量类型的结果

interface Type {
  a: string
  b: number
  c: boolean
  d: undefined
  e: null
  f: never
  h: object
}
type Props = keyof Type // 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'h'
type Test = Type[Props] //string | number | boolean | object | null | undefined
// 注意 never 被排除掉了
// 与上面的用法相对应

注意: 使用keyof或类似keyof这样需要在一些类型中选择类型时会将nullundefinednever类型排除掉

keyof操作符经常与 extends 关键字一起作为泛型的约束来使用

// K只能是T的索引属性组成的一个数组,并且返回一个T属性值对应值组成的数组
function getValue<T, K extends keyof T>(
  obj: T,
  names: K[]
): Array<T[K]> /* 可以写成T[K][] */ {
  return names.map((n) => obj[n])
}
let obj = {
  name: '张三',
  age: 18
}
getValue(obj, ['name', 'age'])

3.2.2 交叉类型

交叉类型是将多个类型合并为一个类型。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

function extend<T, U>(first: T, second: U): T & U {
  let result: T & U = <T & U>{} //只能通过类型断言来做联合类型
  for (let id in first) {
    ;(<any>result)[id] = (<any>first)[id]
  }
  for (let id in second) {
    if (!(result as any).hasOwnProperty(id)) {
      // 如果这里不断言为any会报错的,因为不能确保T&U类型有方法
      ;(<any>result)[id] = (<any>second)[id]
    }
  }
  return result
}
/*
1.简便一点的写法
function extend<T, U>(first: T, second: U): T & U {
  let result: any = {
  for (let id in first) {
    result[id] = first[id]
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      result[id] = second[id]
    }
  }
  return result as T & U
}
2.更简便的
function extend<T, U>(first: T, second: U): T & U {
  let result = {} as T & U
  result = Object.assign(first, second) // 注意要开启es6
  return result
}
*/

class Person {
  constructor(public name: string) {}
}
interface Loggable {
  log(): void
}
class ConsoleLogger implements Loggable {
  log() {
    // ...
  }
}
var jim = extend(new Person('Jim'), new ConsoleLogger())
var n = jim.name
jim.log()

3.2.3 联合类型

联合类型与交叉类型很有关联,但是使用上却完全不同,我们使用的用竖线隔开每一个类型参数的时候使用的就是联合类型

联合类型表示一个值可以是几种类型之一。 用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number,string,或 boolean

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft('Hello world', true) // errors,只能是string或number

注意: 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员

interface Bird {
  fly()
  layEggs()
}

interface Fish {
  swim()
  layEggs()
}

function getSmallPet(): Fish | Bird {
  //这的报错先不管
  // ...
}

let pet = getSmallPet() //因为没有明确返回哪个类型,所以只能确定时Fish和Bird类型中的一个
pet.layEggs() // 我们可以调用共有的方法
pet.swim() // errors,不能调用一个类型的方法,因为万一是Bird类型就不行了

3.2.4 类型保护

联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为 Fish时,JavaScript 里常用来区分 2 个可能值的方法是检查成员是否存在,但是在 TypeScript 中必须要先使用类型断言

let pet = getSmallPet()

if ((<Fish>pet).swim) {
  ;(<Fish>pet).swim()
} else {
  ;(<Bird>pet).fly()
}
  • 用户自定义类型保护

    function isFish(pet: Fish | Bird): pet is Fish {
      return (<Fish>pet).swim !== undefined //返回一个布尔值,然后TypeScript会缩减该变量类型
    }
    /*
    pet is Fish就是类型谓词。谓词为 parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名
    */
    
    // 'swim' 和 'fly' 调用都没有问题了,因为缩减了pet的类型
    if (isFish(pet)) {
      pet.swim()
    } else {
      pet.fly()
    }
    

    注意: TypeScript 不仅知道在 if分支里 petFish类型,它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型

  • typeof 类型保护

    function padLeft(value: string, padding: string | number) {
      if (typeof padding === 'number') {
        return Array(padding + 1).join(' ') + value
      }
      if (typeof padding === 'string') {
        return padding + value
      }
      throw new Error(`Expected string or number, got '${padding}'.`)
    }
    

    注意: 这些 typeof类型保护只有两种形式能被识别:typeof v === "typename"typeof v !== "typename""typename"必须是 "number"" ""boolean""symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护,如使用(typeof str).includes('string')是没有类型保护的

  • instanceof 类型保护

    interface Padder {
      getPaddingString(): string
    }
    
    class SpaceRepeatingPadder implements Padder {
      constructor(private numSpaces: number) {}
      getPaddingString() {
        return Array(this.numSpaces + 1).join(' ')
      }
    }
    
    class StringPadder implements Padder {
      constructor(private value: string) {}
      getPaddingString() {
        return this.value
      }
    }
    
    function getRandomPadder() {
      return Math.random() < 0.5
        ? new SpaceRepeatingPadder(4)
        : new StringPadder('  ')
    }
    
    // 类型为SpaceRepeatingPadder | StringPadder
    let padder: Padder = getRandomPadder()
    
    if (padder instanceof SpaceRepeatingPadder) {
      padder // 类型细化为'SpaceRepeatingPadder'
    }
    if (padder instanceof StringPadder) {
      padder // 类型细化为'StringPadder'
    }
    

    注意:instanceof的右侧要求是一个构造函数,TypeScript 将细化为:

    • 此构造函数的 prototype属性的类型,如果它的类型不为 any的话
    • 构造签名所返回的类型的联合
  • 可以为 null 的类型

    如果没有在 vscode 中,直接编译的话是可以给一个其他类型的值赋值为 undefined 或者 null 的,但是如果编译时使用了--strictNullChecks标记的话,就会和 vscode 一样不能赋值了,并且可选参数和可以选属性会自动加上|undefined类型

    类型保护与类型断言

    由于可以为 null 的类型是通过联合类型实现,那么需要使用类型保护来去除 null

    function f(sn: string | null): string {
      if (sn == null) {
        return 'default'
      } else {
        return sn
      }
    }
    //也可以使用下面的形式
    function f(sn: string | null): string {
      return sn || 'default'
    }
    

    如果编译器不能够去除 nullundefined,可以使用类型断言手动去除。 语法是添加 !后缀:identifier!identifier的类型里去除了 nullundefined

    function broken(name: string | null): string {
      function postfix(epithet: string) {
        return name.charAt(0) + '.  the ' + epithet // error, 'name' is possibly null
      }
      name = name || 'Bob'
      return postfix('great')
    }
    //上面的函数会报错,因为嵌套太深了
    function fixed(name: string | null): string {
      function postfix(epithet: string) {
        //这里的name强制断言不是null或者undefined,因为在嵌套函数中不知道name是否为null
        return name!.charAt(0) + '.  the ' + epithet // ok
      }
      name = name || 'Bob' //这里已经明确表明返回的name肯定是字符串
      return postfix('great') //但是由于是嵌套函数,这里还不知道
    }
    /*
    嵌套函数中:因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。因为它无法跟踪所有对嵌套函数的调用,尤其是将内层函数做为外层函数的返回值。如果无法知道函数在哪里被调用,就无法知道调用时name的类型
    */
    

3.2.5 类型别名

类型别名会给一个类型起个新名字。类型别名有时和接口很像,甚至可以相互兼容,但是类型别名可以作用于原始值,联合类型,元组以及其它任何需要手写的类型,同时类型别名无法像接口一样扩展

注意: 别名不会创建一个新的类型,而是创建了一个新的名字来引用那个类型

type Name = string //通过type关键词创建一个别名
type NameResolver = () => string
type NameOrResolver = Name | NameResolver
function getName(n: NameOrResolver): Name {
  if (typeof n === 'string') {
    return n
  } else {
    return n()
  }
}

同接口一样,类型别名也可以是泛型,可以添加类型参数并且在别名声明的右侧传入

type Container<T> = { value: T }

/*
我们也可以使用类型别名来在属性里引用自己,定义一种可以无限嵌套的树状结构,注意这个应该是可选参数,不然无限引用了
*/
type Tree<T> = {
  value: T
  left?: Tree<T>
  right?: Tree<T>
}

//与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型
type LinkedList<T> = T & { next: LinkedList<T> }

interface Person {
  name: string
}

var people: LinkedList<Person>
var s = people.name //注意这种写法在vscode中会报错,因为更加严格,必须先赋值再使用
var s = people.next.name
var s = people.next.next.name
var s = people.next.next.next.name

注: 类型别名不能直接出现在声明右侧的任何地方

type Yikes = Array<Yikes>
// 报错,提示无限引用本身,只可以在对象属性中引用自己,因为这样才能可选,不然无限嵌套

3.2.6 字面量类型

字面量类型允许指定类型必须的固定值。在实际应用中,字面量类型可以与联合类型,类型保护和类型别名很好的配合。通过结合使用这些特性,可以实现类似枚举类型

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
class UIElement {
  animate(dx: number, dy: number, easing: Easing) {
    if (easing === 'ease-in') {
      // ...
    } else if (easing === 'ease-out') {
    } else if (easing === 'ease-in-out') {
    } else {
      // error! should not pass null or undefined.
    }
  }
}

let button = new UIElement()
button.animate(0, 0, 'ease-in')
button.animate(0, 0, 'uneasy') // error: "uneasy" is not allowed here
//只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误

字面量类型还可以用于区分函数重载

function createElement(tagName: 'img'): HTMLImageElement
function createElement(tagName: 'input'): HTMLInputElement
// ... more overloads ...
function createElement(tagName: string): Element {
  // ... code goes here ...
}

注: 上面只用了字符串做例子,其余的所有类型的固定值也都可适应这个规则

3.2.7 可辨识联合

可辨识联合具有两要素:

  • 具有普通的单例类型属性
  • 一个类型别名包含了这些类型的联合
interface Square {
  kind: 'square'
  size: number
}
interface Rectangle {
  kind: 'rectangle'
  height: number
  width: number
}

interface Circle {
  kind: 'circle'
  radius: number
}

type Shape = Square | Rectangle | Circle
function asserNever(value: never): never {
  throw new Error('Unexpected object:' + value)
}
function getArea(s: Shape): number {
  // s.kind属性就是可辨识联合的索引
  switch (s.kind) {
    // 下面都会自动识别的
    case 'square':
      return s.size * s.size
    case 'rectangle':
      return s.width * s.height
    case 'circle':
      return Math.PI * s.radius * 2
    // 可以定义一个如果都不是就会报错的默认选项,定义了这个后如果没有写上面3个的任意一个都会有清楚的提示
    default:
      return asserNever(s)
  }
}

let a: any = 1
getArea(a) // 抛出错误,注意不是编译器报错

3.2.8 this 类型

this 类型主要用于链式调用中,著名的jQuery就使用了大量返回当前对象this创建链式调用的功能

class BasicCalculator {
  public constructor(protected value: number = 0) {}

  public currentValue(): number {
    return this.value
  }

  public add(operand: number) {
    this.value += operand
    return this
  }

  public subtract(operand: number) {
    this.value -= operand
    return this
  }

  public multiply(operand: number) {
    this.value *= operand
    return this
  }

  public divide(operand: number) {
    this.value /= operand
    return this
  }
}
// 链式调用
let v = new BasicCalculator(2).multiply(5).add(1).currentValue()

在类继承后也可以正常链式调用并且所有的放都会整的反应出来

class BasicCalculator {
  public constructor(protected value: number = 0) {}

  public currentValue(): number {
    return this.value
  }

  public add(operand: number) {
    this.value += operand
    return this
  }

  public subtract(operand: number) {
    this.value -= operand
    return this
  }

  public multiply(operand: number) {
    this.value *= operand
    return this
  }

  public divide(operand: number) {
    this.value /= operand
    return this
  }
}

class ScientificCalculator extends BasicCalculator {
  public constructor(value = 0) {
    super(value)
  }

  public square() {
    this.value = this.value ** 2
    return this
  }

  public sin() {
    this.value = Math.sin(this.value)
    return this
  }
}

let v = new caScientificCalculatorlc(0.5)
  .square()
  .divide(2)
  .sin()
  .currentValue()

注: 在之前,上面调用父类的方法将会报错,只能使用子类的放,TypeScript1.7 增加了 this 类型,那么 divide()返回值类型将会被推断为 this 类型。这就展现了 this 类型的多态,不但可以是父类类型,也可以是子类类型,也可以是实现的接口类型。比如this 类型在描述一些使用了 mixin 风格继承的库 (比如 Ember.js) 的交叉类型

interface MyType {
  extend<T>(other: T): this & T
}

3.2.9 映射类型

映射类型可以理解为我们想要对一个已知的类型进行装饰,类似数组的map方法

注: 映射类型一般是给自变量联合类型使用的

一个常见的任务是将一个已知的类型每个属性都变为可选的:

interface PersonPartial {
  name?: string
  age?: number
}

或者想要变成只读的类型:

interface PersonReadonly {
  readonly name: string
  readonly age: number
}

TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性

// 只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 可选
type Partial<T> = {
  [P in keyof T]?: T[P]
}
type PersonPartial = Partial<Person>
type ReadonlyPerson = Readonly<Person>

下面来看看最简单的映射类型和它的组成部分:

type Keys = 'option1' | 'option2'
/*
k in Keys 类似对象的遍历,只是将联合类型中的每一个类型都遍历出来
*/
type Flags = { [K in Keys]: boolean }

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  • 类型变量 K,它会依次绑定到每个属性。
  • 字符串字面量联合Keys,它包含了要迭代的属性名的集合。
  • 属性的结果类型。
// 上面的答案,会对联合类型进行类型的转换为字面量类型
type Flags = {
  option1: boolean
  option2: boolean
}

注意:k in keyof keysk in keys是有区别的,前者是对类似对象类型的类型使用,后者是对联合类型使用,前面的 keyof 只是把类似对象类型的类型的属性名转变为联合类型再进行后者的操作。并且不能直接对泛型或者类似对象类型使用k in keys这样的语法,会报错,而对联合类型如果使用前者不会报错,但是会造成非正常业务需求的类型错误,因为这样其实 keyof 是将联合类型中的每一个类型中隐藏的 TypeScript 中定义的属性或方法取了出来

// 只读
type Readonly<T> = {
    readonly [P in T]: T[P] // 会报错
    readonly [P in keyof T]: T[P] // 正常业务需求
}
type Pick<T, K extends keyof T> = {
  [P in K]: T[P] // 正常业务需求,返回T中包含K中带有的属性名
}
type Keys = 'option1' | 'option2'
type Flags = { [K in Keys]: boolean } // 正常业务需求
type Flags = { [K in keyof Keys]: boolean } // 非正常业务需求
/*
因为k in Keys就是遍历类型的属性名,而keyof Keys也是拿到属性名,而属性名是字符串类型(和直接keyof string是一样的结果),所以直接遍历的是出来的其实是TypeScript封装的字符串类型中的一系列属性和方法
*/

在真正的应用里,可能不同于上面的 ReadonlyPartial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof索引访问类型要做的事情:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
// keyof Person 代表

但它更有用的地方是可以有一些通用版本。

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是 keyof T且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态]的,映射只作用于 T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial<Person>.name也将是只读的且为可选的

下面是另一个例子, T[P]被包装在 Proxy<T>类里:

type Proxy<T> = {
  get(): T
  set(value: T): void
}
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>
}
function proxify<T>(o: T): Proxify<T> {
  // ... wrap proxies ...
}
let proxyProps = proxify(props)

注意 Readonly<T>Partial<T>用处不小,因此它们与 PickRecord一同被包含进了 TypeScript 的标准库里:

// 四个内置的映射类型

// 只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 可选
type Partial<T> = {
  [P in keyof T]?: T[P]
}
// 获取T中包含K或K数组的属性
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}
// 改变类型,在使用的时候值的类型也会改变
type Record<K extends string, T> = {
  [P in K]: T
}

ReadonlyPartialPick是同态的,但 Record不是。 因为 Record并不需要输入类型来拷贝属性,所以它不属于同态

同态: 两个相同代数结构之间的结构保持映射,也就是说进入和出去的值应该是一样的,而Record进入和出去的值发生了改变

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符

3.2.9.1 对 keyof 的升级

在 2.9 版本中 keyof 操作符已经支持在映射类型的属性值上使用stringnumbersymbol类型

const stringIndex = 'a'
const numberIndex = 1
const symbolIndex = Symbol()
type Obj = {
  [stringIndex]: string
  [numberIndex]: number
  [symbolIndex]: symbol
}

type keyType = keyof Obj

let obj: Readonly<Obj> = {
  a: 'aa',
  1: 11,
  [symbolIndex]: Symbol()
}
3.2.9.2 对于元组和数组的支持

在 3.1 版本中 TypeScript 支持将元组和数组会映射为新的元组和数组,并不会映射为新的类型

type MapToPromise<T> = {
  [K in keyof T]: Promise<T[K]>
}

type Tuple = [number, string, boolean]

type promiseTuple = MapToPromise<Tuple>

const tuple: promiseTuple = [
  new Promise((resolve) => resolve(1)),
  new Promise((resolve) => resolve('a')),
  new Promise((resolve) => resolve(true))
]

3.2.10 由映射类型进行推断

现在了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

function unproxify<T>(t: Proxify<T>): T {
  let result = {} as T
  for (const k in t) {
    result[k] = t[k].get()
  }
  return result
}

let originalProps = unproxify(proxyProps)

注意: 这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数

3.2.11 增加移除修饰符

通过在修辞符前写+-的方法就能够增加和移除修辞符

注意:

  • 这里的修辞符只是readony?这两个能用在所有数据结构地方的修辞符
  • 只能在上面使用了泛型中的K in keyof T这种模式属性周围才能使用增加移除修辞符,也就是必须要in keyof操作符

其实在映射类型中在前面直接添加readonly和在后面加?就是增加修辞符,只是省略了+

type ReadonlyAndPartial<T> = {
  +readonly [P in keyof T]+?: T[P]
}
type WritableAndPartial<T> = {
  -readonly [P in keyof T]-?: T[P]
}

3.2.12 条件类型

条件类型的定义类似 TypeScript 语法中的三元操作符,使用 extends 操作符判断前者是否是后者的子类型之一,语法类似为T extends U ? X:Y

type t<T> = T extends string ? string : number
3.2.12.1 分布式条件类型

当检测的类型为联合类型时,该类型就被叫做分布式条件类型,在实例化的时候 TypeScript 会自动分化为联合类型

type TypeName<T> = T extends any ? T : never
type Tyoe = TypeName<string | number> // string | number

在很多时候这种条件类型会能够帮助我们解决很多事情

type TypeName<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends undefined ? undefined :
T extends () => void ? () => void :
object
type Type = TypeName<() => void> // () => void
type Type = TypeName<(string[]> // object
type Type = TypeName<(() => void) | string[]> // () => void | object
3.2.12.2 条件类型推断

我们现在要实现一个功能,为一个泛型的类型中传入一个类型,如果是数组类型就返回数组中的一个元素的类型,如果不是数组类型就返回该类型

type Type<T> = T extends any[] ? T[number] : T
type Test = Type<string[]> // string
type Test2 = Type<string> // string

而在 TypeScript 中,有一个专门做条件类型推断的关键字inferinfer是用来用做类型推断并赋值的,后面通常跟一个泛型变量,推断后的返回类型交给后面的泛型变量。infer可以专门用来推断出数组中元素的类型

type Type<T> = T extends Array<infer U> ? U : T // 如果成立infer能推断出数组中元素的类型并且赋值给U
type Test = Type<string[]> // string
type Test2 = Type<string> // string
// 效果同上
3.2.12.3 预定义的条件类型

TypeScript 2.8 在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> // "b" | "d"
type T01 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function> // string | number
type T03 = Extract<string | number | (() => void), Function> // () => void

type T04 = NonNullable<string | number | undefined> // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined> // (() => string) | string[]

function f1(s: string) {
  return { a: 1, b: s }
}

class C {
  x = 0
  y = 0
}

type T10 = ReturnType<() => string> // string
type T11 = ReturnType<(s: string) => void> // void
type T12 = ReturnType<<T>() => T> // {}
type T13 = ReturnType<<T extends U, U extends number[]>() => T> // number[]
type T14 = ReturnType<typeof f1> // { a: number, b: string }
type T15 = ReturnType<any> // any
type T16 = ReturnType<never> // any
type T17 = ReturnType<string> // Error
type T18 = ReturnType<Function> // Error

type T20 = InstanceType<typeof C> // C
type T21 = InstanceType<any> // any
type T22 = InstanceType<never> // any
type T23 = InstanceType<string> // Error
type T24 = InstanceType<Function> // Error

注意:Exclude类型是建议的Diff类型的一种实现。使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且感觉这个名字能更好地表达类型的语义。没有增加Omit<T, K>类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>来表示(删除 T 中的 K 有的属性),但要注意 TypeScript 中 3.5 中已经加入了Omit<T, K>类型

3.3 类型断言

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构, 它没有运行时的影响,只是在编译阶段起作用,TypeScript 会假设程序员已经检查过

正则断言有两种方式:

  • 尖括号写法

    let someValue: any = 'this is a string'
    //如果写的是any找长短的时候编译器是找不到length属性的,类型断言后就可以找到
    let strLength: number = (<string>someValue).length
    
  • as 语法

    let someValue: any = 'this is a string'
    let strLength: number = (someValue as string).length
    

注意: 两种形式是等价的,但是当在 TypeScript 里使用 JSX 时,只有as语法断言是被允许的

3.3.1 const 断言

TypeScript 3.4 引入了一种用于文字值的新构造,称为const断言。它的语法是类型断言,const代替类型名(例如123 as const)。当我们使用const断言构造新的文字表达式时,我们可以向语言发出信号:

  • 该表达式中的任何文字类型都不应扩展(例如,不得从"hello"string
  • 对象文字获取readonly属性
  • 数组文字变成readonly元组
// Type '"hello"'
let x = 'hello' as const

// Type 'readonly [10, 20]'
let y = [10, 20] as const

// Type '{ readonly text: "hello" }'
let z = { text: 'hello' } as const

注意:

  • const断言只能立即应用于简单的文字表达式

    // Error! A 'const' assertion can only be applied to a
    // to a string, number, boolean, array, or object literal.
    let a = (Math.random() < 0.5 ? 0 : 1) as const
    
    // Works!
    let b = Math.random() < 0.5 ? (0 as const) : (1 as const)
    
  • const上下文不会立即将表达式转换为完全不可变的

    let arr = [1, 2, 3, 4]
    
    let foo = {
      name: 'foo',
      contents: arr
    } as const
    
    foo.name = 'bar' // error!
    foo.contents = [] // error!
    
    foo.contents.push(5) // ...works!
    

3.4 类型推论

TypeScript 里,在有些没有明确指出类型的地方,类型推论会帮助提供类型

let x = 3
//变量x的类型被推断为数字.这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时

3.4.1 最佳通用类型

当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型

let x = [0, 1, null] //这时x被推断为(null|number)[]

由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型

class Animal {}
class Rhino extends Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}

let zoo = [new Rhino(), new Elephant(), new Snake()]
//被推断为(Rhino | Elephant | Snake)[]类型
//我们想让zoo被推断为Animal[]类型,但是这个数组里没有对象是Animal类型的,因此不能推断出这个结果
//为了更正,需要明确指定类型
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]

3.4.2 上下文类型

TypeScript 类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时

window.onmousedown = function (mouseEvent) {
  //这个例子会报错,因为window.onmousedown已经为event对象设置过类型检查
  console.log(mouseEvent.button) //<- Error
}
/*
	TypeScript类型检查器使用window.onmousedown函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent参数的类型了。如果函数表达式不是在上下文类型的位置,mouseEvent参数的类型需要指定为any,这样也不会报错了
*/
window.onmousedown = function (mouseEvent: any) {
  //手动指定了参数类型上下文类型就会被忽略
  console.log(mouseEvent.button) //<- Now, no error is given
}

上下文归类会在很多情况下使用到。通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型

function createZoo(): Animal[] {
  //在这里面Animal就是被作为最佳通用类型
  return [new Rhino(), new Elephant(), new Snake()]
}

3.4.3 可选链条

TypeScript 3.7 版本中推出的功能,通过可选链条我们能够通过判断某个对象是否有值来进行后续操作。如果该对象有值,就会如同正常的对象属性操作一样向下进行;如果该对象是nullundefined,会直接中断可选链条,返回null或者undefined

let x = foo?.bar.baz()
// 上面的代码与下面功能相同
let x = foo === null || foo === undefined ? undefined : foo.bar.baz()

通过可选链条(?.),我们可以去除大量无用的操作:

// Before
if (foo && foo.bar && foo.bar.baz) {
  // ...
}

// After-ish
if (foo?.bar?.baz) {
  // ...
}

注意:?.操作符只当遇到undefinednull等无效数据上发生短路,而不会在诸如空字符串、0 等有效数据上发生短路。

可选的链条还包括另外两个操作。首先,可选元素访问的作用类似于可选属性访问,但允许我们访问非标识符属性(例如任意字符串、数字和符号):

/**
 * Get the first element of the array if we have an array.
 * Otherwise return undefined.
 */
function tryGetFirstElement<T>(arr?: T[]) {
  return arr?.[0]
  // equivalent to
  //   return (arr === null || arr === undefined) ?
  //       undefined :
  //       arr[0];
}

对于函数来说还有可选链,如果函数不是undefinednull才会对后续函数进行调用:

// 也许我们以前会这样用:
fn && fn()
// 现在可以这样:
fn?.()
async function makeRequest(url: string, log?: (msg: string) => void) {
  log?.(`Request started at ${new Date().toISOString()}`)
  // roughly equivalent to
  //   if (log != null) {
  //       log(`Request started at ${new Date().toISOString()}`);
  //   }

  const result = (await fetch(url)).json()

  log?.(`Request finished at at ${new Date().toISOString()}`)

  return result
}

3.4.3 空合并

空合并运算符是另一个即将推出的 ECMAScript 功能,该功能与可选链条相似,区别是作用于一个单独的值而非属性。

let x = foo ?? bar()
// 上面的功能与下面相同
let x = foo !== null && foo !== undefined ? foo : bar()

在很多情况下,我们可以使用??代替||

// 比如获取 localstorage 中保存的值,我们希望该值不存在的时候默认为0.5
function initializeAudio() {
  let volume = localStorage.volume || 0.5
  // ...
}
// 但是,如果有一次存入的值是 0 ,并且我们也希望获取到该值,使用 || 会让我们丢值这个值
function initializeAudio() {
  // 使用 ?? 能让我们获取到能够使用的数据,避免了可用值丢失的问题
  let volume = localStorage.volume ?? 0.5
  // ...
}

3.5 类型兼容

在 TypeScript 中另一大特性为类型兼容,TypeScript 类型兼容性是基于结构子类型的,同时结构类型只使用其成员来描述类型

3.5.1 属性兼容性

在为对象赋值时,会检测对象中是否含有应该有的属性,同时也会检验额外的属性,如果直接使用对象自变量进行赋值会报错,而如果先赋值给其他变量再给对象赋值就能通过检测。

官方说法: 如果认为 S 相对于 T 具有额外属性,首先 S 是一个fresh object literal type,并且 S 中含有 T 不期望存在的属性。我们这里可以简单的理解为fresh object literal type就是一个直接的对象自变量

fresh object literal types失去 freshness 情况如下:

  • fresh 类型数据被 widened,结果数据的类型失去 freshness
  • 类型断言后产生的数据类型类型失去 freshness
interface Info {
  name: string
}

let info: Info
const info1 = { name: '张三' }
const info2 = { age: 18 }
const info3 = { name: '张三', age: 18 }

info = info1
info = info2 //报错,因为没有name字段
info = info3 // 不会报错
info = { name: '张三', age: 18 } // 直接给会报错
info = { name: '张三', age: 18 } as any // 失去freshness,不会报错

注: 这种检测为递归检测,会对对应的变量的值进行检测

interface Info {
  name: string
  info: { age: number }
}

let info: Info
const info1 = { name: '张三', info: { age: 18 } }
const info2 = { age: 18 }
const info3 = { name: '张三', age: 18 }

info = info1
// info = info2 报错,因为没有name字段
info = info3 // 报错,递归建材到info属性对象没有age属性

3.5.2 函数兼容性

  • 参数兼容

    • 个数兼容: 函数的参数个数不同能够向下兼容,也就是参数个数少的函数能赋值给参数个数多的函数。但是反之就不能成立

      let funcX = (num: number) => 0
      let funcY = (num: number, str: string) => 0
      
      funcY = funcX
      funX = funcY // 报错
      

      比如在 TypeScript 中我们使用数组的forEach方法的时候,我们传入的回调函数可以传 1~3 个参数,如果传多了就会报错,这就是使用回调函数赋值给forEach方法内部使用的案列

      let arr: number[] = [1, 2, 3]
      
      arr.forEach((v, i, a) => {
        console.log(v)
      })
      
      arr.forEach((v) => {
        console.log(v)
      })
      
    • 名称与类型兼容: 参数的名称没必要是相同的,只要与对应的参数类型一致就行,所以要确保参数的类型一致才能赋值

      let funcX = (n: number) => 0
      let funxY = (num: number, str: string) => 0
      
      funxY = funcX
      
      let funcX = (n: string) => 0
      let funxY = (num: number, str: string) => 0
      
      funxY = funcX // 报错,因为对应参数类型不一致
      
    • 可选参数兼容: 被赋值变量上有额外的可选参数不会出错,赋值变量的可选参数在被复制变量里没有对应的参数也不会出错

      let funcX = (ant: number, address?: string, target?: string) => 0
      let funcY = (num: number, str?: string) => 0
      
      funcY = funcX // funcX虽然有两个额外参数,但它们都是可选的,所以不会报错
      
    • 参数双向协变兼容: 默认是兼容的,可以开启严格模式使其不兼容参数双向协变

      let funcX = (num: string | number) => 0
      let funcY = (num: number) => 0
      
      // 默认是兼容的,可以开启严格模式使其不兼容参数双向协变,因为在使用时默认是使用的原来创建函数的类型,funcY只能传入number类型,而funcX是可以接受string和number类型的
      funcY = funcX
      funcX = funcY
      
  • 返回值类型兼容: 函数的返回值同新版的参数双向协变一样,需要源函数的返回值需要能够赋值给目标函数返回值类型

    let funcX = (): string | number => 0
    let funxY = (): number => 0
    
    funcX = funxY
    funY = funcX // 报错
    

    注: 如果目标函数的返回值类型是 void,那么源函数返回值可以是任意类型

    let funcX = (): void => {}
    let funxY = (): number => 0
    
    funcX = funxY // 不会报错
    
  • 函数重载: 对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名(当然也包括必须要函数重载的情况数量一样),规则同上

    function merge(arg1: number, arg2: number): number
    function merge(arg1: string, arg2: string): string
    function merge(arg1: any, arg2: any): any {
      return arg1 + arg2
    }
    
    function sum(arg1: number, arg2: number): number
    function sum(arg1: any, arg2: any): any {
      return arg1 + arg2
    }
    
    let func = merge
    func = sum // 报错
    

3.5.3 枚举兼容性

枚举兼容性在之前已经有提到过,枚举的兼容性可以体现在除了可以被赋值为枚举成员,还能够被赋值给纯数字(必须要枚举成员中有数字类型的成员存在),但是不能被赋值为其他的枚举变量或者字符串

enum Status {
  On,
  Off
}
enum Animal {
  Dog,
  Cat
}
let s = Status.On
s = 1 // 不会报错
s = Animal.Cat // 报错
s = '1' // 报错

3.5.4 类兼容性

TypeScript 类和接口的兼容性非常类似,但是类分实例部分和静态部分。比较两个类类型数据时,只有实例成员会被比较,静态成员和构造函数不会比较

class Animal {
  public static age: number
  constructor(public name: string) {}
}

class People {
  public static age: string
  constructor(public name: string) {}
}

class Food {
  constructor(public name: number) {}
}

let animal: Animal = new People('张三') // 正确,只会比较实例属性方法
let animal2: Animal = new Food('食物') // 报错,因为实例成员类型不正确

注: 实例属性方法比较时不会检验多余的属性和方法

class Animal {
  public static age: number

  constructor(public name: string) {}
}

class People {
  public static age: string
  public gender: string = '男' // 多余的属性
  constructor(public name: string) {}
}

let animal: Animal = new People('张三') // 不会报错
3.5.4.1 类私有成员兼容

私有成员会影响兼容性判断,如果目标类型包含一个私有成员(或受保护类型),那么源类型必须包含来自同一个类的这个私有成员。 允许子类赋值给父类,但是不能赋值给其它有同样类型的类

class Parent {
  constructor(private age: number) {}
}

class Child extends Parent {
  constructor(age: number) {
    super(age)
  }
}

class Other {
  constructor(private age: number) {}
}

let c: Parent = new Child(18)
let other: Parent = new Other(18) // 报错
// 上面的private换成protected也一样

3.5.5 泛型兼容性

TypeScript 是结构性的类型系统,泛型的类型参数影响数据的成员,所以即使进行了泛型的限定如果没有真正的使用给结构中的元素是对数据没有任何影响的

interface Empty<T> {}
let obj: Empty<number> = {} // 不会报错

所以能够这样:

interface Empty<T> {}
let x: Empty<number>
let y: Empty<string> = {}
x = y

但是如果在内部使用了给了成员就无法赋值了

interface Data<T> {
  data: T
}
let x: Data<number>
let y: Data<string> = { data: 'str' }

x = y

3.6 声明合并

TypeScript 中有些独特的概念可以在类型层面上描述 JavaScript 对象的模型。 这其中尤其独特的一个例子是“声明合并”的概念。“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并,并不局限于两个声明

3.6.1 声明的概念

TypeScript 中的声明会创建以下三种实体之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用.符号来访问时使用的名字。 创建类型的声明是用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在 JavaScript 输出中看到的值

Declaration Type(声明类型)Namespace(创建了命名空间)Type(创建了类型)Value(创建了值)
Namespace
Class
Enum
Interface
Type Alias(类型别名)
Function
Variable

3.6.2 接口合并

最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里

interface Box {
  height: number
  width: number
}

interface Box {
  scale: number
}

let box: Box = { height: 5, width: 6, scale: 10 }

注意:

  • 接口的非函数的成员应该是唯一的。如果它们不是唯一的,那么它们必须是相同的类型。如果两个接口中同时声明了同名的非函数成员且它们的类型不同,则编译器会报错

    interface Box {
      height: number
    }
    
    interface Box {
      height: string // 这样是报错的,height只能是number
      width: number
    }
    
  • 对于函数成员,每个同名函数声明都会被当成这个函数的一个重载

    注意: 当接口 A与后来的接口 A合并时,后面的接口具有更高的优先级

    interface Cloner {
      clone(animal: Animal): Animal
    }
    
    interface Cloner {
      clone(animal: Sheep): Sheep
    }
    
    interface Cloner {
      clone(animal: Dog): Dog
      clone(animal: Cat): Cat
    }
    

    合并后的接口:

    interface Cloner {
      clone(animal: Dog): Dog
      clone(animal: Cat): Cat
      clone(animal: Sheep): Sheep
      clone(animal: Animal): Animal
    }
    

    可以看出,每组接口里的声明顺序保持不变,但各组接口之间的顺序是后来的接口重载出现在靠前位置

    但是,这个规则有一个例外是当出现特殊的函数签名时。 如果签名里有一个参数的类型是单一的字符串字面量(比如,不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端

    interface Document {
      createElement(tagName: any): Element
    }
    interface Document {
      createElement(tagName: 'div'): HTMLDivElement
      createElement(tagName: 'span'): HTMLSpanElement
    }
    interface Document {
      createElement(tagName: string): HTMLElement
      createElement(tagName: 'canvas'): HTMLCanvasElement
    }
    

    合并后的接口:

    interface Document {
      createElement(tagName: 'canvas'): HTMLCanvasElement
      createElement(tagName: 'div'): HTMLDivElement
      createElement(tagName: 'span'): HTMLSpanElement
      createElement(tagName: string): HTMLElement
      createElement(tagName: any): Element
    }
    

3.6.3 命名空间合并

对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。 对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里

namespace Animals {
  export class Zebra {}
}

namespace Animals {
  export interface Legged {
    numberOfLegs: number
  }
  export class Dog {}
}

等同于:

namespace Animals {
  export interface Legged {
    numberOfLegs: number
  }

  export class Zebra {}
  export class Dog {}
}

注意: 非导出成员仅在其原有的(合并前的)命名空间内可见。这就是说合并之后,从其它命名空间合并进来的成员无法访问非导出成员

namespace Animal {
  let haveMuscles = true

  export function animalsHaveMuscles() {
    return haveMuscles
  }
}

namespace Animal {
  export function doAnimalsHaveMuscles() {
    return haveMuscles // Error, because haveMuscles is not accessible here
  }
}
/*
因为 haveMuscles并没有导出,只有 animalsHaveMuscles函数共享了原始未合并的命名空间可以访问这个变量。 doAnimalsHaveMuscles函数虽是合并命名空间的一部分,但是访问不了未导出的成员
*/

3.6.4 命名空间与类和函数和枚举类型合并

命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 TypeScript 使用这个功能去实现一些 JavaScript 里的设计模式。

  • 合并命名空间和类: 这让我们可以表示内部类

    class Album {
      label: Album.AlbumLabel
    }
    namespace Album {
      export class AlbumLabel {}
    }
    

    合并规则与上面合并命名空间的规则一致,我们必须导出 AlbumLabel类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 也可以使用命名空间为类增加一些静态属性

    class Person {
        constructor(public name:string){
    	}
    }
    namespace Person {
        export age = 18
    }
    console.log(Person.age) // 18
    
  • 合并命名空间和函数: TypeScript 使用声明合并来达到这个目的并保证类型安全

    function buildLabel(name: string): string {
      return buildLabel.prefix + name + buildLabel.suffix
    }
    
    namespace buildLabel {
      export let suffix = ''
      export let prefix = 'Hello, '
    }
    
    console.log(buildLabel('Sam Smith'))
    
  • 合并命名空间和枚举: 名空间可以用来扩展枚举型

    enum Color {
      red = 1,
      green = 2,
      blue = 4
    }
    // 给Color枚举对象多添加了一个属性mixColor,在这里的效果与给对象赋值是一样的
    namespace Color {
      export function mixColor(colorName: string) {
        if (colorName == 'yellow') {
          return Color.red + Color.green
        } else if (colorName == 'white') {
          return Color.red + Color.green + Color.blue
        } else if (colorName == 'magenta') {
          return Color.red + Color.blue
        } else if (colorName == 'cyan') {
          return Color.green + Color.blue
        }
      }
    }
    
  • 接口和类: 接口可以和类进行合并,合并后类的实例需要具有接口定义的属性或方法

    class Person {}
    
    interface Person {
      name: string
    }
    // 这种声明合并常用在使用类装饰器的时候消除报错
    const c = new Person()
    console.log(c.name) // 不报错,因为已经合并了
    console.log(c.age) // 报错,Person实例没有age属性
    

更多内容

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

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

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