一塊兒學 TypeScript 基礎篇

今年10月初尤雨溪在 GitHub 發佈了 vue3 的 Pre-Alpha 版本源碼,同時大部分源碼使用了 TypeScript 語言進行編寫。能夠說 TypeScript 已經成爲前端開發將來的趨勢。javascript

本篇大部份內容講 TypeScript 的基礎知識,後續內容會更新介紹 TypeScript 在工做中的項目開發及運用。若是您想要獲得最新的更新,能夠點擊下面的連接:html

TypeScript開發教程 文檔版前端

TypeScript開發教程 GitHubvue

什麼是 TypeScript

TypeScript 是一種由微軟開發的自由和開源的編程語言,它是 JavaScript 的一個超集,擴展了 JavaScript 的語法。java

安裝 TypeScript

經過 npm 安裝webpack

$ npm install typescript -g
複製代碼

以上命令會在全局環境下安裝 tsctsserver 兩個命令,安裝完成以後,咱們就能夠在任何地方執行它了。git

tsserver

TypeScript 獨立服務器(又名 tsserver )是一個節點可執行文件,它封裝了 TypeScript 編譯器和語言服務,並經過 JSON 協議公開它們。tsserver 很是適合編輯器和 IDE 支持。es6

通常工做中不經常使用到它。進一步瞭解tsservergithub

tsc

tsc 爲 typescript compiler 的縮寫,即 TypeScript 編譯器,用於將 TS 代碼編譯爲 JS 代碼。使用方法以下:web

$ 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 {} 複製代碼

函數重載分爲兩個部分,第一個部分爲函數重載的列表,也就是第1、二個 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
複製代碼
相關文章
相關標籤/搜索