一起學 TypeScript 基礎篇

D文斌發表於2019-11-21

今年10月初尤雨溪在 GitHub 釋出了 vue3 的 Pre-Alpha 版本原始碼,同時大部分原始碼使用了 TypeScript 語言進行編寫。可以說 TypeScript 已經成為前端開發未來的趨勢。

本篇大部分內容講 TypeScript 的基礎知識,後續內容會更新介紹 TypeScript 在工作中的專案開發及運用。如果您想要得到最新的更新,可以點選下面的連結:

TypeScript開發教程 文件版

TypeScript開發教程 GitHub

什麼是 TypeScript

TypeScript 是一種由微軟開發的自由和開源的程式語言,它是 JavaScript 的一個超集,擴充套件了 JavaScript 的語法。

安裝 TypeScript

通過 npm 安裝

$ npm install typescript -g
複製程式碼

以上命令會在全域性環境下安裝 tsctsserver 兩個命令,安裝完成之後,我們就可以在任何地方執行它了。

tsserver

TypeScript 獨立伺服器(又名 tsserver )是一個節點可執行檔案,它封裝了 TypeScript 編譯器和語言服務,並通過 JSON 協議公開它們。tsserver 非常適合編輯器和 IDE 支援。

一般工作中不常用到它。進一步瞭解tsserver

tsc

tsc 為 typescript compiler 的縮寫,即 TypeScript 編譯器,用於將 TS 程式碼編譯為 JS 程式碼。使用方法如下:

$ tsc index.ts
複製程式碼

編譯成功後,就會在相同目錄下生成一個同名 js 檔案,你也可以通過命令引數來修改預設的輸出名稱。

預設情況下編譯器以 ECMAScript 3(ES3)為目標。可以通過 tsc -h 命令檢視相關幫助,可以瞭解更多的配置。

我們約定使用 TypeScript 編寫的檔案以 .ts 為字尾,用 TypeScript 編寫 React 時,以 .tsx 為字尾。

Hello TypeScript

結合 tsc 命令,我們一起寫一個簡單的例子。

建立一個 index.ts 檔案。

let text: string = 'Hello TypeScript'
複製程式碼

執行 tsc index.ts 命令,會在同目錄下生成 index.js 檔案。

var text = 'Hello TypeScript';
複製程式碼

一個簡單的例子就實現完了。我們可以通過官網提供的 Playground 進行驗證。

但是在專案開發過程中我們會結合構建工具,如 webpack,和對應的本地服務 dev-server 等相關工具一同使用。

接下來把我們瞭解到的知識結合在一起。搭建一個完整的專案

專案根目錄中有一個 tsconfig.json 檔案,簡單介紹其作用。

tsconfig.json

如果一個目錄下存在一個 tsconfig.json 檔案,那麼它意味著這個目錄是 TypeScript 專案的根目錄。tsconfig.json 檔案中指定了用來編譯這個專案的根檔案和編譯選項。 一個專案可以通過以下方式之一來編譯:

  • 不帶任何輸入檔案的情況下呼叫 tsc,編譯器會從當前目錄開始去查詢 tsconfig.json文 件,逐級向上搜尋父目錄。
  • 不帶任何輸入檔案的情況下呼叫 tsc,且使用命令列引數 --project(或 -p )指定一個包含 tsconfig.json 檔案的目錄。

當命令列上指定了輸入檔案時,tsconfig.json檔案會被忽略。

基礎型別

TypeScript 支援與 JavaScript 幾乎相同的資料型別。

JavaScript 資料型別

String、Number、Boolean、Object(Array、Function)、Symbol、undefined、null

TypeScript 新增資料型別

void、any、never、元組、列舉、高階型別

型別註解

作用:相當於強型別語言中的型別宣告

語法:(變數/函式): type

介紹

字串型別

我們使用 string 表示文字資料型別。 和 JavaScript 一樣,可以使用雙引號 " 或單引號 ' 表示字串, 反引號 ` 來定義多行文字和內嵌表示式。

let str: string = 'abc'
複製程式碼

數字型別

和 JavaScript 一樣,TypeScript 裡的所有數字都是浮點數。這些浮點數的型別是 number。 除了支援十進位制和十六進位制字面量,TypeScript 還支援 ECMAScript 2015 中引入的二進位制和八進位制字面量。

let decLiteral: number = 6
let hexLiteral: number = 0xf00d
let binaryLiteral: number = 0b1010
let octalLiteral: number = 0o744
複製程式碼

布林型別

我們使用 boolean 表示布林型別,表示邏輯值 true / false

let bool: boolean = true
複製程式碼

陣列型別

TypeScript 有兩種定義陣列的方式。 第一種,可以在元素型別後加上 []。 第二種,可以使用陣列泛型 Array<元素型別>。 此外,在元素型別中可以使用聯合型別。 符號 | 表示或。

let arr1: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
let arr3: Array<number | string> = [1, 2, 3, 'a']
複製程式碼

元組

元組型別用來表示已知元素數量和型別的陣列,各元素的型別不必相同,對應位置的型別必須相同。

let tuple: [number, string] = [0, '1']
tuple = ['1', 0] // Error
複製程式碼

當訪問一個已知索引的元素,會得到正確的型別:

tuple[0].toFixed(2)
tuple[1].toFixed(2) // Error: Property 'toFixed' does not exist on type 'string'.
複製程式碼

可以呼叫陣列 push 方法新增元素,但並不能讀取新新增的元素。

tuple.push('a')
console.log(tuple) // [0, "1", "a"]
tuple[2] // Error: Tuple type '[number, string]' of length '2' has no element at index '2'.
複製程式碼

列舉

我們使用 enum 表示列舉型別。 列舉成員值只讀,不可修改。 列舉型別是對 JavaScript 標準資料型別的一個補充。C# 等其它語言一樣,使用列舉型別為一組數值賦予友好的命名。

數字列舉

初始值為 0, 逐步遞增,也可以自定義初始值,之後根據初始值逐步遞增。

enum Role {
  Reporter = 1,
  Developer,
  Maintainer,
  Owner,
  Guest
}

console.log(Role.Developer) // 2
console.log(Role[2]) // Developer
複製程式碼

數字列舉會反向對映,可以根據索引值反向獲得列舉型別。原因如下編譯後程式碼所示:

var Role;
(function (Role) {
    Role[Role["Reporter"] = 1] = "Reporter";
    Role[Role["Developer"] = 2] = "Developer";
    Role[Role["Maintainer"] = 3] = "Maintainer";
    Role[Role["Owner"] = 4] = "Owner";
    Role[Role["Guest"] = 5] = "Guest";
})(Role || (Role = {}));
複製程式碼

字串列舉

字串列舉不支援反向對映

enum Message {
  Success = '成功',
  Fail = '失敗'
}
複製程式碼

常量列舉

在列舉關鍵字前新增 const,該常量列舉會在編譯階段被移除。

const enum Month {
  Jan,
  Feb,
  Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar]
複製程式碼

編譯後:

"use strict";
var month = [0 /* Jan */, 1 /* Feb */, 2 /* Mar */]; // [0 /* Jan */, 1 /* Feb */, 2 /* Mar */]
複製程式碼

外部列舉

外部列舉(Ambient Enums)是使用 declare enum 定義的列舉型別。

declare enum Month {
  Jan,
  Feb,
  Mar
}
let month = [Month.Jan, Month.Feb, Month.Mar]
複製程式碼

編譯後:

"use strict";
let month = [Month.Jan, Month.Feb, Month.Mar];
複製程式碼

declare 定義的型別只會用於編譯時的檢查,編譯結果中會被刪除。所以按照上述例子編譯後的結果來看,顯然是不可以的。因為 Month 未定義。

  • declareconst 可以同時存在

物件

TypeScript 有兩種定義物件的方式。 第一種,可以在元素後加上 object。 第二種,可以使用 { key: 元素型別 } 形式。 同樣在元素型別中可以使用聯合型別。注意第一種形式物件元素為只讀。

let obj1: object = { x: 1, y: 2 }
obj1.x = 3 // Error: Property 'x' does not exist on type 'object'.

let obj2: {  x: number, y: number } = { x: 1, y: 2 }
obj2.x = 3
複製程式碼

Symbol

symbol 型別的值是通過 Symbol 建構函式來建立

let s: symbol = Symbol()
複製程式碼

Null & Undefined

null 表示物件值缺失,undefined 表示未定義的值。

let un: undefined = undefined
let nu: null = null
複製程式碼

若其他型別需要被賦值為 nullundefined 時, 在 tsconfig.json 中將 scriptNullChecks 設定為 false。或者 使用聯合型別。

void

用於標識方法返回值的型別,表示該方法沒有返回值。

function noReturn (): void {
  console.log('No return value')
}
複製程式碼
  • undefined 並不是保留欄位可以被賦值,所以設定undefined時,建議使用 void 0

任意型別

宣告為 any 的變數可以賦予任意型別的值。

let x: any
x = 1
x = 'a'
x = {}

let arr: any[] = [1, 'a', null]
複製程式碼

函式

我們先回顧在 JavaScript 中,使用 es6 語法定義一個函式。

let add = (x, y) => x + y
複製程式碼

上面例子中,add 函式有兩個引數 xy 返回其相加之和。 該例子放在 TypeScript 中會提示 引數 xy 隱含一個 any 型別。 所以我們修改如下:

let add = (x: number, y: number): number => x + y
複製程式碼

給引數新增 number 型別,在括號之後也新增返回值的型別。這裡返回值型別可以省略,因為 TypeScript 有型別推斷機制,這個我們之後詳細介紹。

接下來我們使用 TypeScript 定義一個函式型別並實現它。

let plus: (x: number, y: number) => number

plus = (a, b) => a + b

plus(2, 2) // 2
複製程式碼

never

never 型別表示的是那些永不存在的值的型別。 例如,never 型別是那些總是會丟擲異常或根本就不會有返回值的函式表示式或箭頭函式表示式的返回值型別;變數也可能是 never 型別,當它們被永不為真的型別保護所約束時。

never 型別是任何型別的子型別,也可以賦值給任何型別;然而,沒有型別是 never 的子型別或可以賦值給 never 型別(除了 never 本身之外)。 即使 any 也不可以賦值給 never

let error = (): never => {
    throw new Error('error')
}
let endless = (): never => {
    while(true) {}
}
複製程式碼
  • 型別推斷:變數在宣告時並未賦值,型別推斷為 any

介面

在 TypeScript 中,我們可以使用介面 interface 來定義物件型別。

介紹

介面是一系列抽象方法的宣告,是一些方法特徵的集合,這些方法都應該是抽象的,需要由具體的類去實現,然後第三方就可以通過這組抽象方法呼叫,讓具體的類執行具體的方法。

接下來,定義一個簡單的介面:

interface Person {
  name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}
複製程式碼

我們定義了一個介面 Person 和變數 man,變數的型別是 Person。 這樣我們就約束了該變數的值中物件的 keyvalue 要和介面一致。

需要注意的是:

  1. 介面規範首字母大寫;
  2. 被賦值的變數必須和介面的定義保持一致,引數不能多也不能少;
  3. 型別檢查器不會去檢查屬性的順序,只要相應的屬性存在並且型別正確即可。

可選屬性

介面的所有屬性可能都不是必需的。

interface Person {
  name: string
  age?: number
}

let man: Person = {
  name: 'James'
}
複製程式碼

只讀屬性

屬性名前使用 readonly 關鍵字制定為只讀屬性,初始化後不可更改。

interface Person {
  readonly name: string
  age: number
}

let man: Person = {
  name: 'James',
  age: 30
}

man.name = 'Tom' // Error: Cannot assign to 'name' because it is a read-only property.
複製程式碼

任意屬性

用任意的字串索引,使其可以得到任意的結果。

interface Person {
  name: string
  age: number
  [x: string]: any
}

let man: Person = {
  name: 'James',
  age: 30,
  height: '180cm'
}
複製程式碼

除了 nameage 必須一致以外,其他屬性可以隨意定義數量不限。

  • 一旦定義了任意屬性,那麼其他屬性的型別必須是任意屬性型別的子集。
interface Person {
  name: string
  age: number
  [x: string]: string
}

let man: Person = {
  name: 'James',
  age: 30,
  height: '180cm'
}

/**
 * Type '{ name: string; age: number; height: string; }' is not assignable to type 'Person'.
 * Property 'age' is incompatible with index signature.
 * Type 'number' is not assignable to type 'string'.
 */
複製程式碼

數字索引

可以得到任意長度的陣列。

interface StringArray {
  [i: number]: string
}
let chars: StringArray = ['a', 'b']
複製程式碼

介面能夠描述 JavaScript 中物件擁有的各種各樣的外形。 除了描述帶有屬性的普通物件外,介面也可以描述物件型別函式型別

物件型別介面

示例如下:

interface List {
  readonly id: number
  name: string
  age?: number
}

interface Result {
  data: List[]
}

function render (result: Result) {
  console.log(JSON.stringify(result))
}
複製程式碼

首先我們定義了一個 List 物件介面,它的內部有 idnameage 屬性。接下來我們又定義了一個物件介面,這個物件介面有隻一個屬性 data,它型別為 List[]。接下來有一個函式,引數型別為 Result

接下來我們定義一個變數 result,將它傳入 render 函式。

let result = {
  data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
  ]
}

render(result)
複製程式碼

這裡需要注意 data 陣列內的第一個物件裡,增加了一個 sex 屬性,但是在上面的介面定義中沒有 sex 屬性。這時把物件賦給 result 變數,傳入函式,不會被編譯器檢查到。

再看下面的例子:

render({
  data: [
    { id: 1, name: 'A', sex: 'male' },
    { id: 2, name: 'B' }
  ]
})
// Error: Object literal may only specify known properties, and 'sex' does not exist in type 'List'.
複製程式碼

我們將物件字面當做引數傳給了 render 函式時,編譯器會對物件內的屬性進行檢查。

我們可以通過型別斷言規避這個問題

render({
  data: [
    { id: 1, name: 'A', sex: 'male'},
    { id: 2, name: 'B' }
  ]
} as Result)
複製程式碼

除了使用 as 關鍵字,還可以用 <> 符號:

render(<Result>{
  data: [
    { id: 1, name: 'A', sex: 'male'},
    { id: 2, name: 'B' }
  ]
})
複製程式碼

函式型別介面

為了使用介面表示函式型別,我們需要給介面定義一個呼叫簽名。 它就像是一個只有引數列表和返回值型別的函式定義。引數列表裡的每個引數都需要名字和型別。

在資料型別中我們提到過,可以用一個變數宣告一個函式型別。

let add: (x: number, y: number) => number
複製程式碼

此外,我們還可以用介面來定義它。

interface Add {
  (x: number, y: number): number
}

let add: Add = (a, b) => a + b
複製程式碼

除此之外,還有一種更簡潔的方式就是使用型別別名

型別別名使用 type 關鍵字

type Add = (x: number, y: number) => number

let add: Add = (a, b) => a + b
複製程式碼
  • interface 定義函式(Add)和用 type 定義函式(Add)有區別?

typeinterface 多數情況下有相同的功能,就是定義型別。 但有一些小區別:
type:不是建立新的型別,只是為一個給定的型別起一個名字。type還可以進行聯合、交叉等操作,引用起來更簡潔。
interface:建立新的型別,介面之間還可以繼承、宣告合併。建議優先使用 interface

函式

和 JavaScript 一樣,TypeScript 函式可以建立有名字的函式或匿名函式,TypeScript 為 JavaScript 函式新增了額外的功能,讓我們可以更容易的使用它。

在基本型別和介面部分中多多少少提到過函式,接下來總結四種定義函式的方式。

function add (x: number, y: number) {
  return x + y
}

const add: (x: number, y: number) => number

type add = (x: number, y: number) => number

interface add {
  (x: number, y: number) => number
}
複製程式碼

TypeScript 裡的每個函式引數都是必要的。這裡不是指不能把 nullundefined 當做引數,而是說編譯器檢查使用者是否為每個引數都傳入了值。也就是說,傳遞給一個函式的引數個數必須與函式期望的引數個數保持一致。我們舉個例子:

function add (x: number, y: number, z: number) {
  return x + y
}

add(1, 2) // Error: Expected 3 arguments, but got 2.
複製程式碼

在上述例子中,函式定義了3個引數,分別為 xyz,結果返回 xy 的和。並沒有使用引數 z,呼叫 add 只傳入 xy 的值。這時 TypeScript 檢查機制提示預期為三個引數,但實際只傳入兩個引數的錯誤。如何避免這種情況呢?

可選引數

在 TypeScript 裡我們可以在引數名旁使用 ? 實現可選引數的功能。

function add (x: number, y: number, z?: number) {
  return x + y
}

add(1, 2)
複製程式碼

經過修改,引數 z 變為可選引數,檢查通過。

  • 可選引數必須在必選引數之後

預設引數

與 JavaScript 相同,在 TypeScript 裡函式引數同樣可以設定預設值,用法一致。

function add (x: number, y = 2) {
  return x + y
}
複製程式碼

根據型別推斷機制,引數 y 為推斷為 number 型別。

剩餘引數

與 JavaScript 相同。TypeScript 可以把所有引數收集到一個變數裡。

function add (x: number, ...rest: number[]) {
  return x + rest.reduce((prev, curr) => prev + curr)
}

add(1, 2, 3) // 6
複製程式碼
  • 剩餘引數必須在必選引數之後,可選引數不允許和剩餘引數共同出現在一個函式內。

函式過載

TypeScript 的函式過載,要求我們先定義一系列名稱相同的函式宣告。

function add (...rest: number[]): number
function add (...rest: string[]): string
function add (...rest: any[]): any {
  let first = rest[0]
  let type = typeof first
  switch (type) {
    case 'number':
      return rest.reduce((prev, curr) => prev + curr)
    case 'string':
      return rest.join('')
  }
  return null
}
複製程式碼

上面例子中,我們定義了三個相同名稱的函式,引數分別為 numberstringany 型別陣列,相繼返回的型別與引數型別相同。當呼叫該函式時,TypeScript 編譯器能夠選擇正確的型別檢查。在過載列表裡,會從第一個函式開始檢查,從上而下,所以我們使用函式過載時,應該把最容易用到的型別放在最上面。

  • any 型別函式不是過載列表的一部分

傳統的 JavaScript 使用函式和基於原型的繼承來建立可重用的元件。

function Point (x, y) {
  this.x = x
  this.y = y
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')'
}

var p = new Point(1, 2)
複製程式碼

從 ES6 開始,我們能夠使用基於類的物件導向的方式。

class Point {
  constructor (x, y) {
    this.x = x
    this.y = y
  }
  toString () {
    return `(${this.x}, ${this.y})`
  }
}
複製程式碼

TypeScript 除了保留了 ES6 中類的功能以外,還增添了一些新的功能。

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

class Husky extends Dog {
  constructor (name: string, color: string) {
    super(name)
    this.color = color
  }
  color: string
}
複製程式碼

上面的例子中需要注意以下幾點:

  1. 繼承類中的建構函式裡訪問 this 的屬性之前,一定要呼叫 super 方法;
  2. TypeScript 和 ES6 中,“類的成員屬性”都是例項屬性,而不是原型屬性,“類的成員方法”都是“原型”方法。Dog.prototype => {constructor: ƒ, run: ƒ}new Dog('huang') => {name: "huang"}
  3. TypeScript 中例項的屬性必須有初始值,或者在建構函式中被初始化。

public、private、protected、readonly

TypeScript 可以使用三種訪問修飾符(Access Modifiers),分別是 publicprivateprotected

  • public 修飾的屬性或方法是公有的,可以在任何地方被訪問到,預設所有的屬性和方法都是 public

  • private 修飾的屬性或方法是私有的,不能在宣告它的類的外部訪問,包括繼承它的類也不可以訪問

  • protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類中也是允許被訪問

  • 以上三種可以修飾建構函式,預設為 public,當建構函式為 private 時,該類不允許被繼承或例項化;當建構函式為 protected 時,該類只允許被繼承。

  • readonly 修飾的屬性為只讀屬性,只允許出現在屬性宣告或索引簽名中。

public

公共修飾符

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

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom
複製程式碼

private

私有修飾符

class Animal {
  private name: string
  public constructor (name: string) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Error: Property 'name' is private and only accessible within class 'Animal'.

class Cat extends Animal {
  constructor (name: string) {
    super(name)
    console.log(this.name) // Error: // Property 'name' is private and only accessible within class 'Animal'.
  }
}
複製程式碼

需要注意的是,TypeScript 編譯之後的程式碼中,並沒有限制 private 屬性在外部的可訪問性。

上面的例子編譯後的程式碼如下:

var Animal = (function () {
    function Animal (name) {
        this.name = name
    }
    return Animal
}())
var a = new Animal('Jack')
console.log(a.name)
複製程式碼

protected

受保護修飾符

class Animal {
  protected name: string
  public constructor (name: string) {
    this.name = name
  }
}

class Cat extends Animal {
  constructor (name: string) {
    super(name)
    console.log(this.name)
  }
}
複製程式碼
  • 建構函式引數新增修飾等同於在類中定義該屬性,這樣使程式碼更為簡潔。
class Animal {
  // public name: string
  constructor (public name: string) {
    this.name = name
  }
}
class Cat extends Animal {
  constructor (public name: string) {
    super(name)
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom
複製程式碼

readonly

只讀修飾符

class Animal {
  readonly name: string
  public constructor (name: string) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom' //Error: Cannot assign to 'name' because it is a read-only property.
複製程式碼

注意如果 readonly 和其他訪問修飾符同時存在的話,需要寫在其後面。

class Animal {
  // public readonly name: string
  public constructor (public readonly name: string) {
    this.name = name
  }
}
複製程式碼

抽象類

abstract 用於定義抽象類和其中的抽象方法。需要注意以下兩點:

抽象類不允許被例項化

abstract class Animal {
  public name: string
  public constructor (name: string) {
    this.name = name
  }
}

var a = new Animal('Jack') //Error: Cannot create an instance of an abstract class.
複製程式碼

抽象類中的抽象方法必須被繼承類實現

abstract class Animal {
  public name: string;
  public constructor (name: string) {
    this.name = name;
  }
  abstract sayHi (): any
}

class Cat extends Animal {
  public color: string
  sayHi () { console.log(`Hi`) }
  constructor (name: string, color: string) {
    super(name)
    this.color = color
  }
}

var a = new Cat('Tom', 'Blue')
複製程式碼

類與介面

本章節主要介紹類與介面之間實現、相互繼承的操作。

類實現介面

實現(implements)是物件導向中的一個重要概念。一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成介面(interface),用 implements 關鍵字來實現。這個特性大大提高了物件導向的靈活性。

interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
}
複製程式碼
  • 類實現介面時,必須宣告介面中所有定義的屬性和方法。
interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  // eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Property 'eat' is missing in type 'Cat' but required in type 'Animal'.
複製程式碼
  • 類實現介面時,宣告介面中定義的屬性和方法不能修飾為 privateprotected
interface Animal {
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  private name: string
  eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Property 'name' is private in type 'Cat' but not in type 'Animal'.
複製程式碼
  • 介面不能約束類中的建構函式
interface Animal {
  new (name: string): void
  name: string
  eat (): void
}

class Cat implements Animal {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
}

// Error: Class 'Cat' incorrectly implements interface 'Animal'. Type 'Cat' provides no match for the signature 'new (name: string): void'.
複製程式碼

介面繼承介面

實現方法如下:

interface Animal {
  name: string
  eat (): void
}

interface Predators extends Animal {
  run (): void
}

class Cat implements Predators {
  constructor (name: string) {
    this.name = name
  }
  name: string
  eat () {}
  run () {}
}
複製程式碼
  • 繼承多個介面用 , 分割,同理實現多個介面方式相同。
interface Animal {
  name: string
  eat (): void
}
  
interface Lovely {
  cute: number
}

interface Predators extends Animal, Lovely {
  run (): void
}

class Cat implements Predators {
  constructor (name: string, cute: number) {
    this.name = name
    this.cute = cute
  }
  name: string
  cute: number
  eat () {}
  run () {}
}
複製程式碼

介面繼承類

實現方法如下:

class Auto {
  constructor (state: string) {
    this.state = state
  }
  state: string
}

interface AutoInterface extends Auto {}

class C implements AutoInterface {
  state = ''
}
複製程式碼

混合型別

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function(source: string, subString: string) {
  return source.search(subString) !== -1
}
複製程式碼

一個函式還可以有自己的屬性和方法

interface Counter {
  (start: number): string
  interval: number
  reset (): void
}

function getCounter(): Counter {
  let counter = <Counter>function (start: number) {}
  counter.interval = 123
  counter.reset = function () {}
  return counter
}

let c = getCounter()
c(10)
c.reset()
c.interval = 5.0
複製程式碼

小結

  1. 介面與介面、類與類之間可以相互繼承(extends)
  2. 介面可以通過類來實現的(implements),介面只能約束類的公有成員
  3. 介面可以抽離出類的成員、包括公有(public)、私有(private)、受保護(protected)成員

泛型

泛型(Generics)是指在定義函式、介面或類的時候,不預先指定具體的型別,而在使用的時候再指定型別的一種特性。

  • 小技巧:直接把泛型理解為代表型別的引數

簡單的例子

首先,我們來實現一個函式 createArray,它可以建立一個指定長度的陣列,同時將每一項都填充一個預設值:

function createArray(length: number, value: any): Array<any> {
  let result = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
複製程式碼

這段程式碼編譯不會報錯,但是一個顯而易見的缺陷是,它並沒有準確的定義返回值的型別:

Array<any> 允許陣列的每一項都為任意型別。但是我們預期的是,陣列中每一項都應該是輸入的 value 的型別。

這時候,泛型就派上用場了:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray<string>(3, 'x') // ['x', 'x', 'x']
複製程式碼

上例中,我們在函式名後新增了 <T>,其中 T 用來指代任意輸入的型別,在後面的輸入 value: T 和輸出 Array<T> 中即可使用了。

接著在呼叫的時候,可以指定它具體的型別為 string。當然,也可以不手動指定,而讓型別推斷自動推算出來:

function createArray<T>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
複製程式碼

同樣型別陣列也可以被型別推斷。

function log<T> (value: T): T {
  console.log(value)
  return value
}

log<string[]>(['a', 'b'])
// or
log(['a', 'b'])
複製程式碼

多個型別引數

定義泛型的時候,可以一次定義多個型別引數:

function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}

swap([7, 'seven']) // ['seven', 7]
複製程式碼

上例中,我們定義了一個 swap 函式,用來交換輸入的元組。

泛型約束

在函式內部使用泛型變數的時候,由於事先不知道它是哪種型別,所以不能隨意的操作它的屬性或方法。

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length) // Error: Property 'length' does not exist on type 'T'.
  return arg
}
複製程式碼

上例中,泛型 T 不一定包含 length 屬性,所以編譯的時候會報錯。

這時,我們可以對泛型進行約束,只允許這個函式傳入那些包含 length 屬性的變數。這就叫泛型約束

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}
複製程式碼

上例中,我們使用了 extends 約束了泛型 T 必須符合介面 Lengthwise 的形狀,也就是必須包含 length 屬性。

此時如果呼叫 loggingIdentity 函式的時候,傳入的引數不包含 length,那麼在編譯階段就會報錯了。

interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

loggingIdentity(7) // Error: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
複製程式碼

多個型別引數之間也可以相互約束。

function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id]
  }
  return target
}

let x = { a: 1, b: 2, c: 3, d: 4 }

copyFields(x, { b: 10, d: 20 }) // { a: 1, b: 10, c: 3, d: 20 }
複製程式碼

上述例子中,我們使用了兩個型別引數,其中要求 T 繼承 U,這樣就保證了 U 上不會出現 T 中不存在的欄位。

泛型函式

可以用泛型來約束函式的引數和返回值型別。

type Log = <T>(value: T) => T

let log: Log = (value) => {
  console.log(value)
  return value
}

log<number>(2) // 2
log('2') // '2'
log(true) // <boolean>true 
複製程式碼

泛型介面

之前學習過,可以使用介面的方式來定義一個函式需要符合的形狀。

interface SearchFunc {
  (source: string, subString: string): boolean
}

let mySearch: SearchFunc
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1
}
複製程式碼

同樣也可以使用含有泛型的介面來定義函式的形狀。

interface CreateArrayFunc {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
複製程式碼

進一步,我們可以把泛型引數提前到介面名上。

interface CreateArrayFunc<T> {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc<any>
createArray = function<T>(length: number, value: T): Array<T> {
  let result: T[] = []
  for (let i = 0; i < length; i++) {
    result[i] = value
  }
  return result
}

createArray(3, 'x') // ['x', 'x', 'x']
複製程式碼

注意,此時在使用泛型介面的時候,需要定義泛型的型別。

若不想在使用泛型介面時定義泛型的型別,那麼,需要在介面名上的泛型引數設定預設型別。

interface CreateArrayFunc<T = any> {
  <T>(length: number, value: T): Array<T>
}

let createArray: CreateArrayFunc
複製程式碼

泛型類

與泛型介面類似,泛型也可以用於類的型別定義中。

class Log<T> {
  run (value: T) {
    console.log(value)
    return value
  }
}

let log1 = new Log<number>()
log1.run(1) // 1

let log2 = new Log()
log2.run('1') // '1'
複製程式碼
  • 注意: 泛型不能應用於類的靜態成員。
class Log<T> {
  static run (value: T) {
    console.log(value)
    return value
  }
}
// Error: Static members cannot reference class type parameters.
複製程式碼

小結

  1. 函式和類可以輕鬆支援多種型別,增強程式的擴充套件性
  2. 不必寫多條函式過載,冗長的聯合型別宣告,增強程式碼可讀性
  3. 靈活控制型別之間的約束

型別檢查機制

TypeScript 編譯器在做型別檢查時,所秉承的一些原則,以及表現出的一些行為。

本章節分為三大部分:型別推斷型別相容性型別保護

型別推斷

不需要指定變化的型別(函式的返回值型別),TypeScript 可以根據某些規則自動為其推斷出一個型別。

基礎型別推斷

基本型別推斷經常出現在初始化變數的時候。

let a
// let a: any

let a = 1
// let a: number

let a = []
// let a: any[]
複製程式碼

宣告變數 a 時,我們不指定它的型別,ts 就會預設推斷出它是 any 型別。

如果我們將它複製為 1ts 就會推斷出它是 number 型別。

如果我們將它複製為 []ts 就會推斷出它是 any 型別的陣列。

基本型別推斷還會出現在定義函式引數。

let a = (x = 1) => {}
// let a: (x?: number) => void
複製程式碼

宣告函式 a,設定一個引數 x,為它賦值一個預設引數 1,此時 ts 就會推斷出它是 number 型別。同樣返回值型別也會被推斷。

最佳通用型別推斷

當需要從多個型別中推斷出一個型別時,ts 就會盡可能的推斷出一個最佳通用型別。

let a = [1, null]
// let a: (number | null)[]
複製程式碼

宣告一個變數 a,值為一個包含數字 1null 的陣列。此時,變數 a 就被推斷為 numbernull 的聯合型別。

以上的型別推斷都是從右向左的推斷,根據表示式的值推斷出變數的型別。還有一種方式是從左到右,根據上下文推斷。

上下文型別推斷

通常發生在事件處理中。

window.onkeydown = (event) => {
}
// (parameter) event: KeyboardEvent
複製程式碼

window 繫結 onkeydown 事件,引數為 event,此時 ts 會根據左側的事件繫結推斷出右側事件的型別。

型別相容性

當一個型別 Y 可以賦值給另一個型別 X 時,我們可以認為型別 X 相容型別 Y。

X 相容 Y : X (目標型別) = Y (源型別)

變數相容性

let s: string = 'abc'
s = null
複製程式碼

預設會提示 Type 'null' is not assignable to type 'string'. 如果將 tsconfig.json 內的 strictNullChecks 的值設定為 false,這時編譯就不會報錯。

可以說明 string 型別相容 null 型別,nullstring 型別的子型別。

介面相容性

示例如下:

interface X {
  a: any
  b: any
}

interface Y {
  a: any
  b: any
  c: any
}

let x: X = { a: 1, b: 2 }
let y: Y = { a: 1, b: 2, c: 3 }

x = y
y = x // Error: Property 'c' is missing in type 'X' but required in type 'Y'.
複製程式碼

y 可以賦值給 xx 不可以賦值給 y

  • 介面之間相互賦值時,成員少的會相容成員多的。源型別必須具備目標型別的必要屬性。

函式相容性

函式個數

示例如下:

type Handler = (a: number, b: number) => void
function hof(handler: Handler) {
  return handler
}

let handler1 = (a: number) => {}
hof(handler1)

let handler2 = (a: number, b: number, c: number) => {}
hof(handler2)
// Error: Argument of type '(a: number, b: number, c: number) => void' is not assignable to parameter of type 'Handler'.

let handler3 = (a: string) => {}
hof(handler3)
// Error: Types of parameters 'a' and 'a' are incompatible. Type 'number' is not assignable to type 'string'.
複製程式碼

上述示例中,目標型別 handler 有兩個引數,定義了三個不同的函式進行測試。

  1. handler1 函式只有一個引數,將 handler1 傳入 hof 方法作為引數(相容)
  2. handler2 函式有三個引數,同樣作為引數傳入 hof 方法(不相容)。
  3. handler2 函式引數型別與目標函式引數型別不同(不相容)
  • 函式引數個數,引數多的相容引數少的。換句話說,引數多的可以被引數少的替換。

固定引數、可選引數、剩餘引數

示例如下:

// 固定引數
let a = (p1: number, p2: number) => {}
// 可選引數
let b = (p1?: number, p2?: number) => {}
// 剩餘引數
let c = (...args: number[]) => {}

a = b
a = c
b = a // Error
b = c // Error
c = a
c = b
複製程式碼
  • 固定引數相容可選引數和剩餘引數。可選引數不相容固定引數和剩餘引數,如果將 tsconfig.json 內的 strictFunctionTypes 的值設定為 false,這時編譯就不會報錯。剩餘引數相容固定引數和可選引數。

複雜型別

示例如下:

interface Point3D {
  x: number
  y: number
  z: number
}

interface Point2D {
  x: number
  y: number
}
let p3d = (point: Point3D) => {}
let p2d = (point: Point2D) => {}

p3d = p2d
p2d = p3d // Error: Property 'z' is missing in type 'Point2D' but required in type 'Point3D'.
複製程式碼
  • 成員個數多的相容成員個數少的,這裡與介面相容性結論相反。可以把物件拆分為引數,引數多的相容引數少的,與函式相容性結論一致。

如果想要上述示例中的 p2d = p3d 相容。將 tsconfig.json 內的 strictFunctionTypes 的值設定為 false

返回值型別

示例如下:

let f = () => ({ name: 'Alice' })
let g = () => ({ name: 'Alice', location: 'Beijing' })
f = g
g = f // Error
複製程式碼
  • 目標函式的返回值型別,必須與源函式的返回值型別相同,或為其子型別。成員少的相容成員多的。

函式過載

在函式部分中有介紹函式過載,這裡我們重溫一下。

function overload (a: number, b: number): number
function overload (a: string, b: string): string
function overload (a: any, b: any): any {}
複製程式碼

函式過載分為兩個部分,第一個部分為函式過載的列表,也就是第一、二個 overload 函式,也就是目標函式。第二個部分就是函式的具體實現,也就是第三個 overload 函式,也就是源函式。

  • 在過載列表中,目標函式的引數要大於等於源函式的引數。

列舉相容性

示例如下:

enum Fruit { Apple, Banana }
enum Color { Red, Yellow }

let fruit: Fruit.Apple = 3
let no: number = Fruit.Apple

let color: Color.Red = Fruit.Apple // Error
複製程式碼
  • 列舉型別和數值(number)型別相互相容,列舉與列舉之間相互不相容

類相容性

示例如下:

class A {
  constructor (p: number, q: number) {}
  id: number = 1
}

class B {
  static s = 1
  constructor (p: number) {}
  id: number = 2
}

let aa = new A(1, 2)
let bb = new B(1)

aa = bb
bb = aa
複製程式碼
  • 比較類與類是否相容時,靜態成員和建構函式不進行比較。成員少的相容成員多的,父類與子類的例項相互相容。

泛型相容性

示例如下:

interface Empty<T> {}

let obj1: Empty<number> = {}
let obj2: Empty<String> = {}

obj1 = obj2

// 設定屬性

interface Empty<T> {
  value: T
}

let obj1: Empty<number> = { value: 1 }
let obj2: Empty<String> = { value: 'a'}

obj1 = obj2 // Error
複製程式碼
  • 泛型介面未設定任何屬性時,obj1obj2 相互相容,若此時 Empty 設定了屬性 value: T 時,obj1obj2 不相容。

泛型函式

let log1 = <T>(x: T): T => {
  console.log('x')
  return x
}
let log2 = <U>(y: U): U => {
  console.log('y')
  return y
}

log1 = log2
複製程式碼
  • 泛型函式引數型別相同,引數多的相容引數少的。

小結

  1. 結構之間相容,成員少的相容成員多的
  2. 函式之間相容,引數多的相容引數少的

型別保護

TypeScript 能夠在特定的區塊中保證變數屬於某種確定的型別。

可以再此區塊中放心地引用此型別的屬性,或者呼叫此型別的方法。

enum Type { Strong, Week }

class Java {
  helloJava () {
    console.log('hello java')
  }
  java: any
}

class JavaScript {
  helloJavaScript () {
    console.log('hellp javascript')
  }
  javascript: any
}

function getLanguage (type: Type, x: string | number) {
  let lang = type === Type.Strong ? new Java() : new JavaScript()
  if (lang.helloJava) {
    lang.helloJava()
  } else {
    lang.helloJavaScript()
  }
  return lang
}

getLanguage(Type.Strong)
複製程式碼

定義 getLanuage 函式引數 type,判斷 type 為強型別時,返回 Java 例項,反之返回 JavaScript 例項。

判斷 lang 是否有 helloJava 方法,有則執行該方法,反之執行 JavaScript 方法。此時這裡有一個錯誤 Property 'helloJava' does not exist on type 'Java | JavaScript'.

解決這個錯誤,我們需要給 lang 新增型別斷言。

  if ((lang as Java).helloJava) {
    (lang as Java).helloJava()
  } else {
    (lang as JavaScript).helloJavaScript()
  }
複製程式碼

這顯然不是非常理想的解決方案,程式碼可讀性很差。我們可以利用型別保護機制,如下幾個方法。

instanceof

判斷例項是否屬於某個類

if (lang instanceof Java) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
複製程式碼

in

判斷一個屬性是否屬於某個物件

if ('java' in lang) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
複製程式碼

typeof

判斷一個基本型別

if (typeof x === 'string') {
  x.length
} else {
  x.toFixed(2)
}
複製程式碼

建立型別保護函式

function isJava(lang: Java | JavaScript): lang is Java {
  return (lang as Java).helloJava !== undefined
}

if (isJava(lang)) {
  lang.helloJava()
} else {
  lang.helloJavaScript()
}
複製程式碼

高階型別

介紹五種 TypeScript 高階型別:交叉型別聯合型別索引型別對映型別條件型別

這些型別在前面多多少少有被提到過,我們在統一梳理一遍。

交叉型別

& 符號,多個型別合併為一個型別,新的型別具有所有型別的特性。

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
let pet: DogInterface & CatInterface = {
  run () {},
  jump () {}
}
複製程式碼

聯合型別

取值可以為多種型別中的一種

let a: number | string = 1 // or '1'
複製程式碼

字面量聯合型別

let a: 'a' | 'b' | 'c'
let b: 1 | 2 | 3
複製程式碼

物件聯合型別

interface DogInterface {
  run (): void
}
interface CatInterface {
  jump (): void
}
class Dog implements DogInterface {
  run () {}
  eat () {}
}
class Cat implements CatInterface {
  jump () {}
  eat () {}
}
enum Master { Boy, Girl }
function getPet (master: Master) {
  let pet = master === Master.Boy ? new Dog() : new Cat()
  pet.eat()
  return pet
}
複製程式碼

getPet 方法體內的 pet 變數被推斷為 DogCat 的聯合型別。在型別未確定的情況下,只能訪問聯合型別的公有成員 eat 方法。

索引型別

let obj = {
  a: 1,
  b: 2,
  c: 3
}
function getValues (obj: any, keys: string[]) {
  return keys.map(key => obj[key])
}

getValues(obj, ['a', 'b']) // [1, 2]
getValues(obj, ['d', 'e']) // [undefined, undefined]
複製程式碼

keys 傳入非 obj 中的屬性時,會返回 undefined。如何進行約束呢?這裡就需要索引型別。

索引型別的查詢操作符 keyof T 表示型別 T 的所有公共屬性的字面量聯合型別

interface Obj {
  a: number
  b: string
}
let key: keyof Obj // let key: "a" | "b"
複製程式碼

索引訪問操作符 T[K] 物件 T 的屬性 K 代表的型別

let value: Obj['a'] // let value: number
複製程式碼

泛型約束 T extends U

let obj = {
  a: 1,
  b: 2,
  c: 3
}
function getValues <T, U extends keyof T>(obj: T, keys: U[]): T[U][] {
  return keys.map(key => obj[key])
}

getValues(obj, ['a', 'b']) // [1, 2]
getValues(obj, ['d', 'e']) // Type 'string' is not assignable to type '"a" | "b" | "c"'.
複製程式碼

對映型別

可以講一箇舊的型別生成一個新的型別,比如把一個型別中的所有屬性設定成只讀。

interface Obj {
  a: string
  b: number
  c: boolean
}

// 介面所有屬性設定成只讀
type ReadonlyObj = Readonly<Obj>

// 原始碼
/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 介面所有屬性設定成可選
type PartialObj = Partial<Obj>

// 原始碼
/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 抽取Obj子集
type PickObj = Pick<Obj, 'a' | 'b'>

// 原始碼
/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type RecordObj = Record<'x' | 'y' , Obj>
複製程式碼

ts 還有更多內建的對映型別,路徑在 typescript/lib/lib.es5.d.ts 內提供參考。

條件型別

形式為 T extends U ? X : Y,如果型別 T 可以賦值為 U 結果就為 X 反之為 Y

type TypeName<T> =
  T extends string ? 'string' :
  T extends number ? 'number' :
  T extends boolean ? 'boolean' :
  T extends undefined ? 'undefined' :
  T extends Function ? 'function' :
  'object'

type T1 = TypeName<string> // type T1 = "string"
type T2 = TypeName<string[]> // type T2 = "object"
複製程式碼

(A | B) extends U ? X : Y 形式,其約等於 (A extends U ? X : Y) | (B extends U ? X : Y)

type T3 = TypeName<string | number> // type T3 = "string" | "number"
複製程式碼

利用該特性可實現型別過濾。

type Diff<T, U> = T extends U ? never : T

type T4 = Diff<'a' | 'b', 'a'> // type T4 = "b"

// 拆解
// Diff<'a', 'a'> | Diff<'b', 'a'>
// never | 'b'
// 'b'
複製程式碼

根據 Diff 再做擴充。

type NotNull<T> = Diff<T, undefined | null>

type T5 = NotNull<string | number | undefined | null> // type T5 = string | number
複製程式碼

以上 DiffNotNull 條件型別官方已經實現了。

Exclude<T, U> 等於 Diff<T, U>

NonNullable<T> 等於 NotNull<T>

還有更多的官方提供的條件型別,可供大家參考。

// Extract<T, U>
type T6 = Extract<'a', 'a' | 'b'> // type T6 = "a"

// ReturnType<T>
type T7 = ReturnType<() => string> // type T7 = string
複製程式碼

相關文章