TypeScript 知識彙總(一)(3W 字長文)

文章使用的 TypeScript 版本爲3.9.x,後續會根據 TypeScript 官方的更新繼續添加內容,若是有的地方不同多是版本報錯的問題,注意對應版本修改便可。前端

前言

該文章是筆者在學習 TypeScript 的筆記總結,期間尋求了許多資源,包括 TypeScript 的官方文檔等多方面內容,因爲技術緣由,可能有不少總結錯誤或者不到位的地方,還請諸位及時指正,我會在第一時間做出修改。webpack

文章中許多部分的展現順序並非按照教程順序,只是對於同一類型的內容進行了分類處理,許多特性可能會提早使用,若是遇到不懂的地方能夠先看後面內容。程序員

1.什麼是 TypeScript?

TypeScript 的首個版本發行於 2012 年 10 月,後續在不斷的更新中逐漸在前端領域站穩了腳跟,現在絕大多數的框架都或多或少使用 Typescript 來進行開發。es6

TypeScript 的主要特色:web

  • TypeScript 是 JavaScript 的超集,遵循了最新的 ES6 和 ES5 規範,而且擴展了 JavaScript 的語法,引入了諸如接口、泛型、重構等功功能。
  • TypeScript 有着強類型的約束,表明着 TypeScript 有着極高的代碼的可讀性和可維護性,比 JavaScript 更加適合開發大型企業項目。
  • TypeScript 可以直接編譯成 JavaScript ,編譯後的 JavaScript 能夠運行到任何符合版本的瀏覽器上,而且現現在的流行框架均可以繼承 TypeScript。

2.TypeScript 的安裝及編譯

2.1 安裝

能夠經過下面的方式在全局安裝 TypeScripttypescript

npm install -g typescript
# 或 yarn add typescript -g
複製代碼

2.2 編譯

  • 安裝 TypeScript 後經過內置的命令 tsc 就能將 ts 文件編譯爲對應的 js 文件shell

    tsc helloworld.ts
    複製代碼
  • 上面的方法太麻煩,能夠在編譯器(這裏只說在 vscode 中的配置)中自動監視npm

    • 在項目目錄中運行tsc --init生成 tsconfig.json
    • 能夠修改 tsconfig.json 中的outDir選項爲"outDir": "./js",之後全部的 js 代碼都會在這個文件夾中編譯
    • 而後在 vscode 中打開命令面板( Ctrl/Command + Shift + P)選擇輸入task,選擇Run Task,監聽 tsconfig.json,就能夠 vscode 中實時監控 TypeScript 編譯
  • 在工程化項目中可使用 webpack 等打包工具來進行編譯,這裏不過多說明了json

3.TypeScript 的數據類型

TypeScipt 中爲了使編寫的代碼更加規範,更有利於維護,增長了類型校驗,全部之後寫 ts 代碼必需要指定類型canvas

注: 在 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[1] //經過獲取索引獲得標識符
    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
    // value = 123 若是這樣就是類型
    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也能夠直接賦值爲數值,這裏是全部的數值,可是U幣能是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 Test = Type[keyof Type] // string | number | boolean | object
// 與上面的用法相對應
複製代碼

注意: 使用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 字長文)

相關文章
相關標籤/搜索