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 發佈!若是這篇文章幫助到您,記得幫忙點個贊哦~