【学习笔记】TypeScript

160 阅读16分钟

优缺点

优点

缺点

基础类型

布尔 boolean

```ts
let isFlag: boolean = true
```

数值 number

```ts
let decLiteral: number = 20         // 十进制
let hexLiteral: number = 0x14       // 十六进制
let binaryLiteral: number = 0b10100 // 二进制
let octalLiteral: number = 0o24     // 八进制
let goldenSection: number = 0.618   // 浮点数
let notANumber: number = NaN        // 非数字
```

字符串 string

```ts
let name: string = 'helen'
let age: number = 30
let sentence = `Hello, my name is ${name}. I'll be ${age + 1}year old next month`
```

void

当一个函数没有返回值时,可以将其返回值类型定义为 void```ts
function doNothing: void() {}
```
`注`:声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull

null 和 undefined

`undefined``null` 是所有类型的子类型,意思其他类型的变量都能被赋值`undefined``null`

bigint

symbol

数组 array

有以下两种声明方式:

  • 数组元素类型[],推荐使用这种方式
  • Array<数组元素类型>,也称为数组泛型
let list1: number[] = [1, 2, 3]      // 推荐使用
let list2: Array<number> = [1, 2, 3] // 数组泛型

元祖 tuple

存储不同类型的元素,而非像数组那样只能存储相同元素类型(any[] 除外)。

相同类型元素组成成为数组,不同类型元素组成了元组(Tuple)。

let x: [string, number] = ['hello', 10]

元组的类型对应的值必须符合所定义的类型。

枚举 enum

定义一组带有名字的常量。

数字枚举

  • 如果不对枚举常量赋值,则枚举元素中的值类型默认为数字类型,默认从0开始递增。
  • 如果枚举元素中第一个值不为0的数字,则其他值从该数值开始递增
  • 如果枚举元素的某个元素被赋予数值,则该元素往后的值从该元素值开始递增
  • 数字枚举成员的值可以是表达式,但是其紧接的成员值必须被初始化。
enum Color {
    Red,
    Blue = 2,
    Green,
    Yellow = getSomeValue(),
    White = 5 // 这里不初始化则会报错
}
Color.Red === 0 // true
Color.Blue === 2 // true
COlor.Green === 3 // true

字符串枚举

指将所有的枚举成员的用字符串进行初始化。

异构枚举 Heterogeneous enums

枚举成员的值包含 number 类型和 string 类型。不建议使用。

object VS enum

const enum EDirection {
    Up,
    Down,
    Left,
    Right
}
const ODirection = {
    Up: 0,
    Down: 1,
    Left: 2,
    Right: 3
} as const

任意 any

在不能确定变量类型的情况下,我们不希望类型检查器对这些值进行检查,而是直接让它们通过编译阶段的检查,此时可以使用 any。

  • any 类型的变量可以被赋予任何类型的值
  • any 类型的变量类似object对象,可以以对象属性的方式访问其存在或不存在的属性
  • any 类型的变量无需事先执行任何类型的检查

:不建议使用这种数据类型,不便于维护代码。

对象 object

object类型表示非原始类型,数组、元素、枚举和普通对象都是object类型。

let obj: object

enum gender {
    male = 1,
    female = 2
}

obj = gender
obj = [1, 2, 3]
obj = ['a', 1]
obj = { name: 'lily'}

never 和 unknown

  • never 类型表示那些永不存在的值的类型。

    never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。

    使用场景:

    • 一个抛出异常的函数表达式,其函数返回值类型为 never
    • 不会有返回值的函数表达式,其函数返回值类型也为 never
    • 不能取得值的地方
  • unknown 类型是 any 类型对应的安全类型。

    • unknown 类型的变量可以被赋予其他类型的值
    • unknown 类型的变量只能分配给 any 类型和 unknown 类型本身

    :在那些将取得任意值,但不知道具体类型的地方使用 unknown,而非 any。

类 class

new 操作符会创建一个对象,并调用 contructor 构造函数初始化这个对象。

特性

继承 extends

使用继承来扩展类,使用关键字 extends 实现继承,被继承的类也叫做 基类或超类,继承的类叫做 派生类或子类

    class Animal {
      move(distance: number = 0) {}
    }
    class Dog extends Animal {
      bark() {
        console.log('Woof')
      }
    }
    const dog = new Dog()
    dog.bark()
    dog.move(10)

如果在子类中包含了一个 constructor,则这个子类在 constructor 中使用this之前必须调用 super(),相当于调用了超类的 constructor,这是ES6中一条强制执行的规则,在TS同样也是强制执行的一条规则。

公共访问修饰符 public

在TS中,类中的所有成员默认都是 public,可以自由地访问这些 public 成员。

    class Animal {
        public name: string
        public constructor(name: string) {
            this.name = name
        }
        public move(distance: number = 0) {
            console.log(`${this.name} moved ${distance}m`)
        }
    }

私有访问修饰符 private

private 修饰的成员有几个特点:

  • 只能在声明它的类内部使用
  • 不能被继承
  • 实例对象也不能访问到
  • 可以通过实例方法来间接访问 private 成员
    class Animal {
        private name: string
        construcotr(name: string) {
            this.name = name
        }
        getName() {
            return this.name
        }
    }
    
    class Dog extends {
        constructor(name: string) {
            super(name)
        }
    }
    
    const cat = new Animal('cat')
    const dog = new Dog('旺财')
    console.log(cat.name)      // 提示错误,name仅能在 Animal 内部使用
    console.log(cat.getName()) // cat
    console.log(dog.name)      // 提示错误,name仅能在 Animal 内部使用
    console.log(dog.getName()) // 旺财

保护访问修饰符 protected

protected 修饰的成员和 private 的区别是,protected 修饰的成员可以在子类中可以访问。

特点:

  • protected 成员不可以在类的外部访问,但是可以被继承,即可以在子类中访问。
  • protected 修饰的构造函数,表明该类不可以被实例化,但是可以被继承。
  • 可以通过实例方法来间接访问该成员

只读修饰符 readonly

只读属性只能在声明时或构造函数中被初始化,一但初始化就不能再修改。只读除了初始化和不能被修改之外,其余性质和其他成员一样可以被继承被访问到。

参数属性

定义:通过给构造函数的参数前面添加一个访问限定符来声明一个属性,并且这个成员会被这个访问限定符修饰。

    class Animal {
        constructor(readonly name: string) {
        }
    }
    
    class Animal {
        constructor(private name: string) {
        }
    }
        

存取器 getters/setters

通过 getters/setters 来截取对对象成员的访问,有效控制对对象成员的访问。

    let password = '123456'
    class Employee {
        private _fullName: string
        get fullName() string {
            return this._fullName
        }
        set fullName(newName: string) {
            if (password && password === '123456') {
                this._fullName = newName
            } else {
                console.error('Error: 密码错误')
            }
        }
    }
    
    let employee = new Employee()
    employee.fullName = '章三'

特点:

  • 要求编译器设置输出 ES5+
  • 只有 get,没有 set 的存取器自动被推断为 readonly

类的静态成员 static

特点:

  • 是类成员,只能通过类名访问,在类内部也要通过类名来访问
  • 不能被继承

抽象类 abstract

有抽象类、抽象方法。

  • 与接口不同的是,抽象类内部可以包含成员的实现细节。
  • 抽象类中的抽象方法不包含具体实现,并且必须在子类中实现

高级技巧

  • 声明一个类时,同时创建了该类实例对象的类型,即该类

把类当作接口使用

类定义会创建:

  • 类的实例类型
  • 构造函数

可以在定义接口的时候,继承该类,把这个类当作接口一样使用。

函数

函数有两种类型:

  • 命名函数
    function add(num1: number, num2: number): number {
        return num1 + num2
    }
    
  • 匿名函数
    let add: (num1: number, num2: number) => number = function(num1, num2) {
        return num1 + num2
    }
    

函数类型

函数类型包含2个部分:

  • 参数类型
  • 返回值类型

ts中完整的函数形式是包含参数类型和返回值类型:

let myAdd: (x: number, y: number) => number = function(x: number, y: number): number { return x + y; };

推断类型

在赋值语句左边指定了类型,右边没有指定类型会自动推断出参数类型和返回值类型。

可选参数和默认参数

TS 默认情况是要求函数参数都是必须的,即函数调用时必须为每个参数都传入值。

JS 中参数是可选,如果不传入对应参数的值,则该参数值默认是 undefined

可选参数 ?

TS 中使用 ? 跟在参数名后名,表示该参数是可选的,可选参数必须在所有必须参数的后面。

function getFullName(firstName: string, lastName?: string) {
   return firstName + lastName
}
getFullName('Jhon') // 正确
getFullName('Jhon', 'Smith') // 正确

默认参数

function getFullName(firstName: string, lastName = 'Smith') {
   return firstName + lastName
}
getFullName('Jhon') // 正确
getFullName('Jhon', 'Smith') // 正确

特点:

  • 默认参数不需要放在必须参数后面

    这样的默认参数在使用时可传入 undefined 使其默认值生效。

  • 所有必须参数后面的默认参数都是可选的

    也就是说可选参数与末尾的默认参数共享参数类型。

    function getFullName(firstName: string, lastName?: string) { // }
    function getFullName(firstName: string, lastName = 'Smith') { // }
    

    上述两个函数共享同一个函数类型:

    (firstName: string, lastName?: string) => string
    

剩余参数 ...rest

必须参数、默认参数和可选参数都表示的是某一个参数。

JS 中使用 arguments 来获取所有参数,也可以使用剩余变量...rest来获取剩余参数列表,其中rest表示的是剩余参数变量。

在TS中,同样可以使用剩余参数变量获取剩余的参数。

特点:

  • 表示:...rest,其中 rest 是剩余参数变量名,可以是任意的名字。
  • 剩余参数变量是一个数组类型
function getFullName(firstName: string, ...restOfName: string[]): string {
    return firstName + restOfName.jon(' ')
}

// 函数类型
let myFullName: (firstName: string, ...resetOfName: string[]) => string = getFullName

this

JS 中,this 只有在函数被调用时才指定。

ES6中,使用箭头语法来创建函数时,this 在函数创建时就指定了,而不是在调用时指定。

重载

JS 中函数会根据不同的参数类型来返回不同的值。

TS 中为同一个函数提供不同的函数类型定义来进行函数重载。

function pickCard(x: {suit: string; card: number; }[]): number
function pickCard(x: number): {suit: string; card: number; }

function pickCard(x): any {
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length); 
        return pickedCard; 
    } 
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 }; 
    } 
}

重载函数的调用过程:

查找重载列表,尝试使用第一个重载定义,如果匹配则使用,否则继续查找下一个定义。因此要把参数最具体的放在最前面。

pickCard(x): any 不是重载列表的一部分。上面的pickCard函数的重载列表只有上面的两个。

类型推论

类型推断发生的时间

  • 初始化变量和成员时
  • 函数的参数设置默认值和返回值时

通用类型

计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

两种情况:

  • 候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。

    这时需要将这个共享的通用类型指定为这个变量的类型。

    let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]
    
  • 不能为所有候选类型找到共享相同的通用类型,则候选类型的联合数组类型。

    type Animal = Rhino | Elephant | Snake
    let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]
    

上下文类型

发生位置:表达式的类型与所处的位置相关时。

window.onmousedown = function(mouseEvent) {
    console.log(mouseEvent.button) // 报错
}

TS 类型检查器使用winsow.onmousedown 来推断右边函数表达式的类型,能够推断出 mouseEvent 的类型。

  • 如果函数表达式不是在上下文的位置,则需要讲参数类型指定为 any
  • 如果函数表达式包含了明确的类型信息,则上下文类型被忽略。

使用场景:

  • 函数参数
  • 赋值表达式的右边
  • 类型断言
  • 对象成员
  • 数组字面量
  • 返回值语句

类型兼容性

TS 里的类型兼容是基于结构子类型的。

了解以下几个概念:

  • 名义类型:通过明确的声明和类型的名称来决定的。
  • 结构类型:是一种只使用成员来描述类型的方式,不要求明确声明。
  • 超集 superset,js中的父类(也称超类、基类)
  • 子集 subset,js中的子类(也称派生类)

要了解 typescript 中的类型兼容性,要从以下几个方面学习:

  • 协变 covariant:子集可以赋值给超集
    • 父类 = 子类(正确)
    • 子类 = 父类(错误)
  • 逆变 contravariant,只针对函数参数有效,--strictFunctionTypes 开启时,只支持逆变
    • 父类 = 子类(错误)
    • 子类 = 父类(正确)
  • 双向协变 bivariant,只针对函数参数有效,--strictFunctionTypes 关闭时,支持双向协变
    • 父类 = 子类(正确)
    • 子类 = 父类(正确)
  • 不变 invariant

对象/类兼容性(协变 convariant

子类类型 赋值 给父类类型,因为可以访问到共同的成员。

反过来的话,父类类型 赋值给 子类类型,由于父类中没有子类中特有的成员,当访问子类中特有的成员时就会报错,所以 父类类型不可以赋值给子类类型

class Animal {
    doAnimalThing() {
        console.log('do animal thing')
    }
}
class Cat extends Animal {
    doCatThing() {
        console.log('喵喵')
    }
}
function foo(animal: Animal) {
    animal.doAnimalThing()
}
foo(new Animal()) // ok
foo(new Cat()) // ok 子类类型 赋值给 父类类型

function bar(animal: Cat) {
    animal.doCatThing()
}
bar(new Cat()) // ok
bar(new Animal()) // Error,Animal类型中没有doCatThing这个方法

Cat类型继承了Animal类型的 doAnimalThing方法,所以传入 Cat类型的对象时能够正确执行。

interface Named {
    name: string
}
let y = { name: '章三', location: '中国' }
let x: Named
x = y // ok

在对象兼容性赋值操作时,只有目标类型的成员会被一一检查是否都在源类型中。

这个比较的过程时递归进行的,检查每个成员及其子成员。

函数兼容性

函数参数兼容性(逆变和双向协变 contravariant bivariant
  • 逆变 contravariant

    let x = (a: number) => 0
    let y = {b: number, s: string) => 0
    
    y = x // ok
    x = y // error
    
    • 根据JS中函数参数可以省略的规则,参数列表的源类型成员必须要在参数列表的目标类型中找到对应的参数,成员名字可以不同,成员类型必须相同,即源类型成员可以比目标类型成员个数少。

    • 第二个例子中,源类型类型有一个必须成员,而目标类型中没有该成员,不可以赋值,相当于函数调用时参数可以少,但是不能多。

  • 双向协变 bivariant

    开启 strictFunctionTypes,会禁用函数参数的双向协变。

函数返回值兼容性(协变 covariant)
  • 参考对象或原始类型的兼容性赋值
  • 源函数的返回值类型必须是目标函数返回值类型的子类型

枚举兼容性

  • 数字枚举类型和数字类型兼容
  • 不同枚举类型之间不兼容
// 数字枚举
enum Color {
    Red,
    Blue
}
// 字符串枚举
enum WeekDay {
    Monday = 'monday',
    Theusday = 'theusday'
}
const c: Color = 1 // ok
let w: WeekDay
w = 1 // error
w = 'monday' // error
w = Color.Red // error
w = WeekDay.Monday // ok

类兼容性

参考对象类型兼容性(协变),不过类只比较类的实例部分,静态成员和构造函数不参与比较。

泛性兼容性

比较之前需要指定泛型参数,否则泛型参数默认为 any 类型。

高级类型

交叉类型(Intersection Types) &

含义:使用 & 符号将多个类型合并为一个类型,新的类型包含了合并前的所有类型

使用这种类型要都是交叉类型中子类型的类型。

联合类型(Union Types) |

interface vs type

类型别名 Type Aliases

任意类型起一个名字

  • 主要是为一个类型起另外一个名字来使用。
type stringAliases = string // 为 string 类型另外取了一个名字,使用时还是 string 类型。

接口 intereface

接口声明是命名对象类型的一种方式。

相同点和区别

相同点:

  • 都可以进行扩展
    • 接口使用 extends 进行扩展
    interface Animal {
        name: string
    }
    interface Bear extends Animal {
        honey: boolean
    }
    const bear = getBear()
    bear.name // ok
    bear.honey // ok
    
    • 类型别名使用 & 来进行扩展
    type Animal {
        name: string
    }
    type Bear = Animal & {
        honey: boolean
    }
    const bear = getBear()
    bear.name // ok
    bear.honey // ok
    

区别:

  • 接口可以通过声明同名接口的方式来合并接口,达到为已有接口添加成员的目的。
    interface Window {
        title: string
    }
    interface Window {
        ts: TypeScriptAPI
    }
    
  • 类型别名不可声明同名的类型别名

类型断言

你比 typescript 更确定是具体什么类型时可以使用类型断言来进行断言某个变量或参数时什么类型。

类型有两种表示方式

  • 使用 as
    const myCanvas = document.getElementById('canvas') as HTMLCanvasElement
    
  • 使用 <type>variable
    const myCanvas = <HTMLCanvasElement>document.getElementById('canvas')
    

这个例子中,ts 只知道会返回 HTMLElement,但是我们可以确定返回的是 HTMLCanvasElement,只有这样我们才能使用这个类型特有的方法 getContext('2d').

注意

  • 和类型注解一样,类型断言会在编译阶段被移除,不会对运行时代码造成影响。

  • 类型断言只能把一个类型转化为更特别或者更不特定的类型。

    类型a转化为类型b,b应该是a的子集

    const x = 'hello' as number // 报错,提示 string 和 number 没有重叠部分
    

    可以先将 string as unknown

    const x = 'hello' as unknown as number // ok
    

    即这种格式:

    const a = (expresssion as any) as T
    

字面量类型

字符串和数字字面量类型

除了一般的字符串和数字类型,我们还可以在类型位置指定特定的字符串和数字。

声明方式:

const constantString: 'Hello' // constantString 只能被赋予 'Hello'

这样的字面量类型没有什么价值,但是使用联合类型的方式组合多个字面量类型则更有用了:

const direction: 'left' | 'right' | 'top' | 'down'

这个例子中,direction 可以取上面四个字符串值

同样,数字字面量也可以,并且字面类型还可以和其他非字面量类型进行联合使用。

字面量推断

function handleRequest(url: string, method: 'GET' | 'POST') {
    // ...
}
const req = { url: 'https://example.com', method: 'GET' }
handleRequest(req.url, req.method) // 报错

req.methodstring 类型,而 handlemethod 参数是 'GET'|'POST' 字面量的联合类型。

  • 可以使用类型断言来进行处理:

    const req = { url: 'https://example.com', method: 'GET' as 'GET' }
    // 或
    handleRequest(req.url, req.method as 'GET')
    
  • 使用 const 来转成字面量类型

    const req = { url: 'https://example.com', method: 'GET' as 'GET' } as const
    

    as const 作用是确保所有的对象属性都被转成字面量类型。

null 和 undefined

ts 中的 null 和 undefined 是否可以用,取决于 strictNullChecks的配置:

  • off:null 和 undefined 可以正常使用,null和undefined可以赋给任意类型。
  • on:会对空值进行检查

非空断言擦作符 !

在任意表达式后面添加 ! 可以断言该值不会是 nullundefined

function liveDangerously(x? number | null) {
    console.log(x!.toFixed())
}

枚举 enum

缩小类型范围

typeof 限定类型

true 值限定

  • &&

  • ||

  • !

  • if

    if 语句强制 boolean 运算为 false 值有:

    • 0
    • NaN
    • ''
    • 0n (bigInt类型的0)
    • null
    • undefined 与之相反的值都是 boolean true

相等限定

  • ===
  • !==
  • ==
  • !=

in 操作符

判断某个对象自身是否有某个属性字段。

instanceof

在 JS 中,x instanceof y,会检查 x.prototype 中是否包含 y.prototype 来确定 x 是否是 y的实例。