聊聊TypeScript類型聲明那些最佳實踐

頭圖

TypeScript 誕生已久,優缺點你們都知曉,它能夠說是JavaScript靜態類型校驗和語法加強的利器,爲了更好的代碼可讀性和可維護性,咱們一個個老工程都坦然接受了用TypeScript 重構的命運。然而在改造的過程當中,逐步意識到TypeScript這門語言的藝術魅力前端

人狠話很少,下面咱們先來聊一下 TypeScript 類型聲明相關的技巧:segmentfault

先了解TypeScript的類型系統

TypeScript是 JavaScript 的超集,它提供了 JavaScript的全部功能,並在這些功能的基礎上附加一層:TypeScript的類型系統數組

圖片

什麼TypeScript的類型系統呢?舉個簡單的例子,JavaScript 提供了 String、Number、Boolean等基本數據類型,但它不會檢查變量是否正確地匹配了這些類型,這也是 JavaScript 弱類型校驗語言的天生缺陷,此處可能會有人DIS 弱類型語言的那些優勢。但無能否認的是,不少大型項目裏因爲這種 弱類型的隱式轉換 和 一些不嚴謹的判斷條件 埋下了不勝枚舉的 BUG,固然這不是咱們今天要討論的主題。數據結構

不一樣於JavaScript,TypeScript 能實時檢測咱們書寫代碼裏 變量的類型是否被正確匹配,有了這一機制咱們能在書寫代碼的時候 就提早發現 代碼中可能出現的意外行爲,從而減小出錯機會。 類型系統由如下幾個模塊組成:函數

推導類型

首先,TypeScript 能夠根據 JavaScript 聲明的變量 自動生成類型(此方式只能針對基本數據類型),好比:工具

const helloWorld = 'Hello World'  // 此時helloWorld的類型自動推導爲string

定義類型

再者,若是聲明一些複雜的數據結構,自動推導類型的功能就顯得不許確了,此時須要咱們手動來定義 interface:ui

const helloWorld = { first: 'Hello', last: 'World' } // 此時helloWorld的類型自動推導爲object,沒法約束對象內部的數據類型

// 經過自定義類型來約束
interface IHelloWorld {
  first: string
  last: string
}
const helloWorld: IHelloWorld = { first: 'Hello', last: 'World' }

聯合類型

能夠經過組合簡單類型來建立複雜類型。而使用聯合類型,咱們能夠聲明一個類型能夠是許多類型之一的組合,好比:url

type IWeather = 'sunny' | 'cloudy' | 'snowy'

泛型

泛型是一個比較晦澀概念,但它很是重要,不一樣於聯合類型,泛型的使用更加靈活,能夠爲類型提供變量。舉個常見的例子:.net

type myArray = Array // 沒有泛型約束的數組能夠包含任何類型

// 經過泛型約束的數組只能包含指定的類型
type StringArray = Array<string> // 字符串數組
type NumberArray = Array<number> // 數字數組
type ObjectWithNameArray = Array<{ name: string }> // 自定義對象的數組

除了以上簡單的使用,還能夠經過聲明變量來動態設置類型,好比:code

interface Backpack<T> {
  add: (obj: T) => void
  get: () => T
}
declare const backpack: Backpack<string>
console.log(backpack.get()) // 打印出 「string」

結構類型系統

TypeScript的核心原則之一是類型檢查的重點在於值的結構,有時稱爲"duck typing" 或 "structured typing"。即若是兩個對象具備相同的數據結構,則將它們視爲相同的類型,好比:

interface Point {
  x: number
  y: number
}

interface Rect {
  x: number
  y: number
  width: number
  height: number
}

function logPoint(p: Point) {
  console.log(p)
}
const point: Point = { x: 1, y: 2 }
const rect: Rect = { x:3, y: 3, width: 30, height: 50 }

logPoint(point) // 類型檢查經過
logPoint(rect) // 類型檢查也經過,由於Rect具備Point相同的結構,從感官上說就是React繼承了Point的結構

此外,若是對象或類具備全部必需的屬性,則TypeScript會認爲它們成功匹配,而與實現細節無關

分清type和interface的區別

interface 和 type 均可以用來聲明 TypeScript 的類型, 新手很容易搞錯。咱們先簡單羅列一下二者的差別:

對比項 type interface
類型合併方式 只能經過&進行合併 同名自動合併,經過extends擴展
支持的數據結構 全部類型 只能表達 object/class/function 類型

注意:因爲 interface 支持同名類型自動合併,咱們開發一些組件或工具庫時,對於出入參的類型應該儘量地使用 interface 聲明,方便開發者在調用時作自定義擴展

從使用場景上說,type 的用途更增強大,不侷限於表達 object/class/function ,還能聲明基本類型別名、聯合類型、元組等類型:

// 聲明基本數據類型別名
type NewString = string

// 聲明聯合類型
interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
type SmallPet = Bird | Fish

// 聲明元組
type SmallPetList = [Bird, Fish]

3個重要的原則

TypeScript 類型聲明很是靈活,這也意味着一千個莎士比亞就能寫出一千個哈姆雷特。在團隊協做中,爲了更好的可維護性, 咱們應該儘量地踐行如下3條原則:

泛型優於聯合類型

舉個官方的示例代碼作比較:

interface Bird {
  fly(): void
  layEggs(): boolean
}
interface Fish {
  swim(): void
  layEggs(): boolean
}
// 得到小寵物,這裏認爲不可以下蛋的寵物是小寵物。現實中的邏輯有點牽強,只是舉個例子。
function getSmallPet(...animals: Array<Fish | Bird>): Fish | Bird {
  for (const animal of animals) {
    if (!animal.layEggs())
      return animal
  }
  return animals[0]
}

let pet = getSmallPet()
pet.layEggs() // okay 由於layEggs是Fish | Bird 共有的方法
pet.swim() // errors 由於swim是Fish的方法,而這裏可能不存在

這種命名方式有3個問題:

  • 第一,類型定義使 getSmallPet變得侷限。從代碼邏輯看,它的做用是返回一個不下蛋的動物,返回的類型指向的是Fish或Bird。但我若是隻想在一羣鳥中挑出一個不下蛋的鳥呢?經過調用這個方法,我只能獲得一個 多是Fish、或者是Bird的神奇生物。
  • 第二,代碼重複、難以擴展。好比,我想再增長一個烏龜,我必須找到全部相似 Fish | Bird 的地方,而後把它修改成 Fish | Bird | Turtle
  • 第三,類型簽名沒法提供邏輯相關性。咱們再審視一下類型簽名,徹底沒法看出這裏爲何是 Fish | Bird 而不是其餘動物,它們兩個到底和邏輯有什麼關係纔可以被放在這裏

介於以上問題,咱們可使用泛型重構一下上面的代碼,來解決這些問題:

// 將共有的layEggs抽象到Eggable接口
interface Eggable {
  layEggs(): boolean
}

interface Bird extends Eggable {
  fly(): void
}
  
interface Fish extends Eggable {
  swim(): void
}
  
function getSmallPet<T extends Eggable>(...animals: Array<T>): T {
  for (const animal of animals) {
    if (!animal.layEggs()) return animal
  }
  return animals[0]
}
  
let pet = getSmallPet<Fish>()
pet.layEggs()
pet.swim()

巧用typeof推導優於自定義類型

這個技巧能夠在沒有反作用的代碼中使用,最多見的是前端定義的常量數據結構。舉個簡單的case,咱們在使用Redux的時候,每每須要給Redux每一個模塊的State設置初始值。這個地方就能夠用typeof推導出該模塊的數據結構類型:

// 聲明模塊的初始state
const userInitState = {
  name: '',
  workid: '',
  avator: '',
  department: '',
}

// 根據初始state推導出當前模塊的數據結構
export type IUserStateMode = typeof userInitState // 導出的數據類型能夠在其餘地方使用

這個技巧可讓咱們很是坦然地 「偷懶」,同時也能減小一些Redux裏的類型聲明,比較實用

巧用內置工具函數優於重複聲明

Typescript提供的內置工具函數有以下幾個:

內置函數 用途 例子
Partial<T> 類型T的全部子集(每一個屬性均可選) Partial<IUserStateMode>
Readony<T> 返回和T同樣的類型,但全部屬性都是隻讀 Readony<IUserStateMode>
Required<T> 返回和T同樣的類型,每一個屬性都是必須的 Required<IUserStateMode>
Pick<T, K extends keyof T> 從類型T中挑選的部分屬性K `Pick<IUserStateMode, 'name'
Exclude<T, U extends keyof T> 從類型T中移除部分屬性U `Exclude<IUserStateMode, 'name'
NonNullable<T> 從屬性T中移除null和undefined NonNullable<IUserStateMode>
ReturnType<T> 返回函數類型T的返回值類型 ReturnType<IUserStateMode>
Record<K, T> 生產一個屬性爲K,類型爲T的類型集合 Record<keyof IUserStateMode, string>
Omit<T, K> 忽略T中的K屬性 Omit<IUserStateMode, 'name'>

上面幾個工具函數尤爲是 Partial、Pick、Exclude, Omit, Record 很是實用,平時在編寫過程當中能夠作一些刻意練習

參考資料

本文由博客一文多發平臺 OpenWrite 發佈!若是這篇文章幫助到您,記得幫忙點個贊哦~

相關文章
相關標籤/搜索