二、TypeScript语法细节

178 阅读9分钟

一、联合类型和交叉类型

1. 联合类型

  • TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型
  • 联合类型(Union Type)
    • 联合类型是由两个或多个其它类型组成的类型
    • 表示可以是这些类型中的任何一个值
    • 联合类型中每一个类型被称为联合成员(union' members)

2. 联合类型的使用

  • 传入给一个联合类型的值是非常简单的:只要保证是联合类型中的某一个类型的值即可
    • 拿到这个值后,如何使用它?因为它可能是任何一种类型
    • 如我们拿到的值可能是 string 或 number,就不能对其调用string上的一些方法
  • 该如何处理这样的问题?
    • 需要缩小(narrow)联合类型
    • TypeScript 可以根据我们缩小的代码结构,推断出更加具体的类型

二、type和interface使用

1. 类型别名 type

  • 通过在类型注解中编写对象类型和联合类型,当我们要多次在其它地方使用时,就要编写多次,很不方便
  • 此时,可以给对象类型起一个别名复用
type Point = {
    x: number
    y: number
    z?: number
}

2. 接口的声明

  • 前面讲了通过 type 来声明一个对象类型
  • 对象的另外一种声明方式就是通过接口来声明
interface Point {
    x: number
    y: number
}
  • 二者的区别
    • 类型别名和接口非常相似,在定义对象类型时,可以任意选择使用
    • 接口的几乎所有特性都可以在 type 中使用

3. interface 和 type 区别

  • interface 和 type 都可以用来定义对象类型
    • 如果定义非对象类型,通常推荐使用 type,比如Direction、Alignment、一些Function
  • 如果定义的是对象类型,区别如下
    • interface 可以重复的对某个接口来定义属性和方法
    • type 定义的是别名,别名是不能重复的
  • 所以,interface 可以为现有的接口提供更多的扩展
    • 接口还有很多其他的用法

4. 交叉类型

  • 联合类型表示多个类型中的一个即可
  • 交叉类型(Intersection Types)
    • 交叉类型表示需要满足多个类型的条件
    • 交叉类型使用 & 符号
  • type MyType = number & string
    • 表达式含义是 number 和 string 要同时满足
    • 但没有同时满足 number、string 的值,所以 MyType 其实就是一个 never类型
  • 交叉类型的应用
    • 在开发中,进行交叉时,通常是对对象类型进行交叉的
interface Colorful {
    color: string
}
interface IKun {
    running: () => void
}
type NewType = Colorful & IKun

const obj: NewType = {
    color: 'red',
    running: function() {}
}

三、类型断言和非空断言

1. 类型断言 as

  • 有时候 TypeScript 无法获取具体的类型信息,此时需要使用类型断言(Type Assertions)
    • 比如通过 document.getElementById,TypeScript 只知道该函数会返回 HTMLElement,并不知道具体的类型
const img = document.getElementById('.img') as HTMLImageElement

img.src = 'xxx'
  • TypeScript 只允许类型断言转换为更具体不太具体(any、unknown)的类型版本,此规则可防止不可能的强制转换

2. 非空类型断言!

  • 如下代码在执行ts的编译阶段会报错
    • 这是因为传入的 msg 有可能是 undefined,这时是不能执行方法的
function getMsg(msg?: string) {
    // error ...
    console.log(mesg.toUpperCase())
}
getMsg("haha")
  • 但,我们确定传入的参数是有值的,这个时候可以使用非空类型断言
    • 非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过 ts 在编译阶段对它的检测
function getMsg(msg?: string) {
    console.log(mesg!.toUpperCase())
}

四、字面量类型和类型缩小

1. 字面量类型

  • 字面量类型
let msg: "Hello Vue" = "Hello Vue"
  • 有何意义?
    • 将多个类型联合在一起
type Alignment = 'left' | 'right' | 'center'

2. 字面量推理

  • 如下代码会报错
const info = {
    url: 'xxx',
    method: 'GET'
}

function request({url: string, method: 'GET' | 'POST'}) {
    console.log(url, method)
}

request(info.url, info.method)
  • 这是因为我们的对象在进行字面量推理的时候,info其实是一个 {url: string, method: string},所以没有办法将一个 string赋值给一个字面量类型

3. 类型缩小

  • 什么是类型缩小?
    • Type Narrowing(又称类型收窄)
    • 可以通过类似 typeof padding === 'number' 的判断语句,来改变 TypeScript 的执行路径
    • 在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称之为 缩小(Narrowing)
    • 编写的 typeof padding === 'number' 称为 类型保护(type guards)
  • 常见的类型保护有如下几种
    • typeof
      • 在 TypeScript 中,检查返回的值 typeof 是一种类型保护
      • 因为 TypeScript 对如何 typeof 操作不同的值进行编码
    • 平等缩小(如:===、!==)
      • 可以使用 switch 或 相等 的一些运算符来表达相等性(如:===、!==、==、!=)
    • instanceof
      • JavaScript 有一个运算符来检查一个值是否是另一个值的 实例
    • in
      • JavaScript 有一个运算符,用于确定对象是否具有带名称的属性:in 运算符
      • 如果指定的属性在指定的对象或其原型链中,则 in 运算符 返回 true
    • 等等

五、函数的类型和函数签名

1. TypeScript 函数类型

  • 在 JavaScript 开发中,函数是重要的组成部分,函数可以作为一等公民(可以作参数,可以作返回值进行传递)
  • 在使用函数的过程中,函数也有自己的类型
  • 函数类型的表达式(Function Type Expressions),来表示函数类型
type Func = (num1: number, num2: number) => void

2. TypeScript 函数类型解析

  • (num1: number, num2: number) => void,代表的就是一个函数类型
    • 接受两个参数的函数,并且都是 number 类型
    • 函数没有返回值,是 void
  • 在某些语言中,可能参数名 num1 和 num2 是可以省略的,但是 TypeScript 中不可以省略

3. 调用签名(Call Signatures)

  • 在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的
    • 然而上面写的函数类型表达式并不支持声明属性
    • 如果想要描述一个带有属性的函数,可以在一个对象类型中写一个调用签名(Call signature)
interface Func {
    name: string
    (num1: number, num2: number): void
}

function calc(calcFn: Func) {
    calcFn(1, 2)
}
  • 注意:这个语法跟函数类型表达式稍有不同,在参数列表和返回的类型之间用的是 冒号: ,而不是 =>

4. 构造签名(Construct Signatures)

  • JavaScript 函数也可以使用 new 操作符调用,当被调用时,TypeScript 会认为这是一个构造函数(constructors),会产生一个新对象
    • 写一个构造签名(Construct Signatures),方法是在调用签名签名加上一个 new 关键词
interface IPerson {
    new (name: string): Person
}

function func(ctor: IPerson) {
    return new ctor("haha")
}

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

func(Person)

5. 参数的可选类型

  • 指定某个参数是可选的
// 此时,参数 y 依旧有类型,类型是 number | undefined
function func(x: number, y?:number) {
    console.log(x, y)
}
  • 另外可选类型需要在必传参数的后面

6. 默认参数

  • 从 ES6 开始,JavaScript 是支持默认参数的,TypeScript 也是支持默认参数的
function func(x: number, y: number = 10) {
    console.log(x, y)
}
func(1)
  • 此时 y 的类型其实是 undefined 和 number 类型的联合

7. 剩余参数

  • 从 ES6 开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中
function sum(...nums: number[]) {
    let total = 0
    for (const num of nums) {
        total += num
    }
    return total
}
const res = sum(1,2,3,4,5)
console.log(res)

六、函数的重载和this类型

1. 函数的重载(了解)

  • 在 TypeScript 中,编写一个 add 函数,希望可以对字符串和数字类型进行相加
    • 使用联合类型得类型缩小
  • 编写
    • TypeScript 中,可以去编写不同的重载签名(overload signatures)来表示函数可以以不同的方式进行调用
    • 一般是编写两个或以上的重载签名,再去编写一个通用的函数以及实现

2. sum函数的重载

  • 对 sum 函数进行重构
    • 在调用 sum 的时候,它会根据传入的参数类型决定执行函数体时,到底执行哪一个函数的重载签名
function sum(num1: number, num2: number): number;
function sum(num1: string, num2: string): string;
function sum(num1: any, num2: any): any {
    return num1 + num2
}

console.log(sum(1, 2))
console.log(sum('a', 'b'))
  • 注意:有实现体的函数,是不能直接被调用的

3. 联合类型和重载

  • 需求:定义一个函数,可以传入字符串或数组,获取长度
  • 两种实现方案
    • ①:使用联合类型来实现
    • ②:实现函数重载来实现
function getLength(a: string | any[]) {
    return a.length
}

function getLength(a: string): number
function getLength(a: any[]): number
function getLength(a: any) {
    return a.length
}
  • 在开发中,尽量选择使用联合类型来实现

4. 可推导的 this 类型

  • 目前在 Vue3 和 React 开发中不一定使用到this
    • Vue3 Composition API中很少见到 this
    • React 的 Hooks 开发中也很少见到 this
  • 如下代码在默认情况下是可以正常运行的,在 TypeScript 编译时,认为我们的 this 是可以正确去使用的
    • 在没有指定this的情况下,this默认情况下是any类型的

5. this 的编译选项

  • VSCode 在检测 TypeScript 代码时,默认情况下运行不确定的 this 按照 any 类型去使用
    • 可以创建一个 tsconfig.json 文件,并在其中告知 VSCode 必须明确执行(不能是隐式的)
    • tsconfig.json => noImplicitThis: true
  • 在设置了 noImplicitThis 为 true 时,TypeScript 会根据上下文推导this,但是在不能正确推导时,就会报错的指定this

6. 指定 this 的类型

  • 在开启 noImplicitThis 的情况下,必须制定 this 的类型
  • 使用 函数的第一个参数类型来指定
    • 函数的第一个参数可以根据该函数之后被调用的情况,用于声明this的类型(名字必须使用 this)
    • 在后续调用函数传入参数时,从第二个参数开始传递的,this参数会在编译后被抹除
function foo(this: { name: string }) {
    console.log(this)
}
foo.call({ name: 'ikun' })

7. this 相关的内置工具

  • TypeScript 提供了一些工具类型来辅助进行常见的类型转换,这些类型全局可用
  • ThisParameterType
    • 用于提取一个函数类型 Type 的this参数类型
    • 如果这个函数类型没有this参数,返回unknown
function foo(this: { name: string }) {
    console.log(this.name)
}
type ThisType = ThisParameterType<typeof foo>
  • OmitThisParameter
    • 用于移除一个函数类型 Type 的this参数类型,并且返回当前的函数类型
type FnType = OmitThisParameter<typeof foo>
  • ThisType
    • 这个类型不返回一个转换过的类型,他被用作标记一个上下文的this类型
interface IState {
    name: string
    age: number
}
interface IStore {
    state: IState
    eating: () => void
    running: () => void
}
const store: IStore & ThisType<IState> = {
    state: {
        name: 'ikun',
        age: 17
    },
    eating: function() {
        console.log(this.name)
    },
    runing: function() {
        console.log(this.name)
    }
}

store.eating.call(store.state)