TypeScript常常用到的高級類型

前言

ts入門了一段時間,但是碰上一些高級類型老是一頭霧水,還須要去看文檔,因此總結了一些工做中常常用到的一些高級類型。因此閱讀以前你須要掌握一點兒ts基礎。typescript

1、交叉類型

交叉類型將多個類型合併爲一個新的類型,新的具備全部參與合併的類型的特性,本質上是一種並的操做。形式以下:安全

T & U

//例子:
function extend<T , U>(first: T, second: U) : T & U {
    let result: <T & U> = {}
    for (let key in first) {
        result[key] = first[key]
    }
    for (let key in second) {
        if(!result.hasOwnProperty(key)) {
            result[key] = second[key]
        }
    }
    return result
}
複製代碼

2、聯合類型

聯合類型用 | 分割每個類型,表示只能取其中的一種類型,好比: number | string | boolean 的類型只能是這三個的一種,不能共存。 若是一個值的類型是聯合類型,那麼咱們只能訪問它們中共有的屬性或者方法,本質上是一種交的關係,好比:bash

interface Cat {
    eat(food) : void
    miao() : string
}
interface Dog {
    eat(food) : void
    wang() : string
}

function getPet() : Cat | Dog {
    ...
}
let pet = getPet()
pet.eat() //正確
pet.miao() //報錯
複製代碼

3、類型保護與類型區分

聯合類型讓一個值能夠爲不一樣的類型,但隨之帶來的問題就是訪問非共同方法時會報錯。那麼該如何區分值的具體類型,以及如何訪問共有成員?函數

1.使用類型斷言

let pet = getPet()
if((<Cat>pet).miao) {
    (<Cat>pet).miao()
} else {
    (<Dog>pet).wang()
}
複製代碼

可見使用類型斷言就不得不寫一堆蹩腳的尖括號,那麼尚未更簡潔明瞭辦法來判斷類型呢?ui

2.使用類型保護

function isCat(pet: Cat | Dog) : pet is Cat {
    return (<Cat>pet).miao !== undefined
}
複製代碼

這種param is SomeType 的形式就是類型保護,咱們能夠用它來明確一個聯合類型變量的具體形式,在調用時ts就會將變量縮減爲該具體類型,調用就是合法的了this

if (isCat(pet)) {
    pet.miao() //正確
} else {
    pet.wang()
}
複製代碼

容許這麼作是由於:typescript可以經過類型保護知道if語句裏的pet類型必定是Fish類型,並且else語句裏的pet類型必定不是Fish類型,那麼就是Bird類型了spa

3.type 和instanceof

當咱們使用了typeof和instanceof後,typescript就會自動限制類型爲某一具體類型,從而咱們能夠安全地在語句體內使用具體類型的方法和屬性,如:prototype

function show(param: number | string) {
    if (typeof param === 'number') {
        console.log(`${param} is number`)
    } else {
        console.log(`${param} is string`)
    }
}
複製代碼

可是typeof只支持number、string、boolean或者symbol(只有這些狀況下能夠被認爲是類型保護)code

若是是類,咱們能夠用instanceof對象

let an = getSomeKindType()
if (an instanceof Animal) {
    an // 此時aa會細化爲Animal類型
}
if (an instanceof People) {
    an //此時aa會細化爲People類型
}
複製代碼

ts要求instanceof右側是一個構造函數,而ts會將其細化爲:

  • 此構造函數的prototype屬性的類型
  • 構造函數所返回的類型的聯合

4. 可爲null的類型

null和undefined能夠賦給任何的類型,由於它們是全部其餘類型的一個有效值,如:

// 如下語句均合法
let x1: number = null
let x2: string = null
let x3: boolean = null
let x4: undefined = null
let y1: number = undefined
let y2: string = undefined
let y3: boolean = undefined
let y4: null = undefined
複製代碼

在typescript裏,咱們可使用--strictNullChecks標記,開啓這個標記後,當咱們聲明一個變量時,就不會自動包含null或undefined,可是咱們能夠手動使用聯合類型來明確包含,如:

let x = 123
x = null // 報錯
let y: number | null = 123
y = null // 容許
y = undefined // 報錯,`undefined`不能賦值給`number | null`
複製代碼

當開啓了--strictNullChecks,可選參數/屬性就會被自動地加上| undefined,如:

function foo(x: number, y?: number) {
    return x + (y || 0)
}
foo(1, 2) // 容許
foo(1) // 容許
foo(1, undefined) // 容許
foo(1, null) // 報錯,不容許將null賦值給`number | undefined`類型
複製代碼

4、類型別名

類型別名能夠給現有的類型起個新名字,它和接口很像但又不同,由於類型別名能夠做用於原始值、聯合類型、元組及其餘任何須要手寫的類型,語法如:

type Name = string

別名不會新建一個類型,它只會建立一個新的名字來引用現有類型。因此在VSCode裏將鼠標放在別名上時,顯示的是所引用的那個類型

1.範性別名

type Container<T> = {
    value: T
}

let name: Container<string> = {
    value: 'hello'
}
複製代碼

2. 和接口的區別

  • 別名不能被extends和implements
  • 錯誤信息、鼠標懸停時,不會使用別名,而是直接顯示爲所引用的類型

3. 字符串字面量類型

字符串字面量類型容許咱們定義一個別名,類型爲別名的變量只能取固定的幾個值,如:

type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
let x1: Easing = 'uneasy' // 報錯: Type '"uneasy"' is not assignable to type 'Easing'
let x2: Easing = 'ease-in' // 容許
複製代碼

4. 可辨識聯合

能夠合併字符串字面量類型、聯合類型、類型保護和類型別名來建立可辨識聯合的高級模式(也稱爲標籤聯合或者代數數據類型),具備3個要素:

  • 具備普通的字符串字面量屬性——可辨識的特徵
  • 一個類型別名,用來包含了那些類型的聯合——聯合
  • 此屬性上的類型保護

建立一個可辨識聯合類型,首先須要聲明將要聯合的接口,每一個接口都要有一個可辨識的特徵,如(kind屬性):

interface Square {
    kind: 'square'
    size: number
}

interface Rectangle {
    kind: 'rectangle'
    width: number
    height: number
}

interface Circle {
    kind: 'circle'
    radius: number
}
複製代碼

如今,各個接口之間仍是沒有關聯的,因此咱們須要使用類型別名來聯合這幾個接口,如:

type Shape = Square | Rectangle | Circle

複製代碼

如今,使用可辨識聯合,如:

function area(s: Shape) {
    switch (s.kind) {
        case 'square':
            return s.size * s.size
        case 'rectangle':
            return s.height * s.width
        case 'circle':
            return Math.PI * s.radius ** 2
    }
}
複製代碼

5、多態的this類型

多態的this類型表示的是某個包含類或接口的子類型,例子如:

class BasicCalculator {
    public constructor(protected value: number = 0) {
    }
    public currentValue(): number {
        return this.value
    }
    public add(operand: number): this {
        this.value += operand
        return this
    }
    public multiply(operand: number): this {
        this.value *= operand
        return this
    }
}

let v = new BasicCalculator(2).multiply(5).add(1).currentValue() // 11
複製代碼

因爲使用了this類型,當子類繼承父類的時候,新的類就能夠直接使用以前的方法,而不須要作任何的改變,如:

class ScientificCalculator extends BasicCalculator {
    public cconstructor(value = 0) {
        super(value)
    }
    public sin() {
        this.value = Math.sin(this.value)
        return this
    }
}
let v = new BasicCalculator(2).multiply(5).sin().add(1).currentValue()
複製代碼

若是沒有this類型,那麼ScientificCalculator就不可以在繼承BasicCalculator的同時還保持接口的連貫性。由於multiply方法會返回BasicCalculator類型,而BasicCalculator沒有sin方法。然而,使用this類型,multiply就會返回this,在這裏就是ScientificCalculator

6、索引類型

索引類型能使編譯器可以檢查使用了動態屬性名的代碼,好比咱們想要寫一個函數,它能夠選取對象中的部分元素的值,那麼:

function pluck<T, K entends keyof T>(o: T, names: K[]) : T[K][] {
    return names.map(n => o[n])
}

interface Person {
    name: string
    age: number
}

let p: Person = {
    name: 'LL',
    age: 18
}
let res = pluck(p, ['name']) //容許
複製代碼

以上代碼解釋以下:

  • 首先,使用keyof關鍵字,它是索引類型查詢操做符,它可以得到任何類型T上已知的公共屬性名的聯合。如例子中,keyof T至關於'name' | 'age'
  • 而後,K extends keyof T代表K的取值限制於'name' | 'age'
  • 而T[K]則表明對象裏相應key的元素的類型,因此在例子中,p對象裏的name屬性,是string類型,因此此時T[K]至關於Person[name],即至關於類型string,因此返回的是string[],因此res的類型爲string[]

因此,根據以上例子,觸類旁通有:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key]
}
let obj = {
    name: 'LL',
    age: 18,
    male: true
}
let x1 = getProperty(obj, 'name') // 容許,x1的類型爲string
let x2 = getProperty(obj, 'age') // 容許,x2的類型爲number
let x3 = getProperty(obj, 'male') // 容許,x3的類型爲boolean
let x4 = getProperty(obj, 'hobby') // 報錯:Argument of type '"hobby"' is not assignable to parameter of type '"name" | "age" | "male"'.
複製代碼
索引類型和字符串索引簽名

keyof和T[K]與字符串索引簽名進行交互,若是有一個帶有字符串索引簽名的類型,那麼keyof T爲string,且T[string]爲索引簽名的類型,如:

interface Demo<T> {
    [key: string]: T
}
let keys: keyof Demo<boolean> // keys的類型爲string
let value: Demo<number>['foo'] // value的類型爲number
複製代碼

7、映射類型

咱們可能會遇到這麼一些需求: 1)將一個現有類型的每一個屬性都變爲可選的,如:

interface Person {
    name: string
    age: number
}
複製代碼

可選版本爲:

interface PersonPartial {
    name?: string
    age?: number
}
複製代碼

2)或者將每一個屬性都變爲只讀的,如:

interface PersonReadonly {
    readonly name: string
    readonly age: number
}
複製代碼

而如今typescript爲咱們提供了映射類型,可以使得這種轉化更加方便,在映射類型裏,新類型將以相同的形式去轉換舊類型裏每一個屬性,如以上例子能夠改寫爲:

type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}//ts 源碼
type Partial<T> = {
    [P in keyof T]?: T[P]
}//ts源碼
type PersonReadonly = Readonly<Person>
type PersonPartial = Partial<Person>
複製代碼

另外ts還給咱們提供了Required,Pick,Record,幾種經常使用到的類型,下面是它們的源碼:

type Required<T> = {
    [P in keyof T]-?: T[P];
}

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

type Record<K extends keyof any, T> = {
    [P in K]: T;
}

type personRequired = Required<Person>;
// personRequired === {name: string; age: number}

type personPick = Pick<Person, "name">;
// person5 === {name: string}

type personRecord = Record<'name' | 'age', string>
// personRecord === {name: string; age: string}
複製代碼

咱們還能夠寫出更多的通用映射類型,如:

// 可爲空類型
type Nullable<T> {
    [P in keyof T]: T[P] | null
}

// 包裝一個類型的屬性
type Proxy<T> = {
    get(): T
    set(value: T): void
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>
}
function proxify(o: T): Proxify<T> {
    // ...
}
let proxyProps = proxify(props)
複製代碼

由映射類型進行推斷(拆包)

上面展現瞭如何包裝一個類型,那麼與之相反的就有拆包操做,示例如:

function unproxify<T>(t: Proxify<T>): T {
    let result = <T>{}
    for (const k in t) {
        result[k] = t[k].get()
    } 
    return result
}
let originalProps = unproxify(proxyProps)
複製代碼
相關文章
相關標籤/搜索