[譯]使用 Typescript 使無效狀態不可恢復

      有一種好的 Haskell 編程原則,一樣也是一種好的函數式編程原則,叫作使無效狀態不可恢復原則。這是什麼原則呢?一般咱們使用類型系統來構建對數據和狀態施加約束的類型,從而達到能夠表明已存在狀態的效果。如今,在類型級別上,咱們設法消除了無效狀態,但類型系統每次試圖構造無效狀態時都會介入無效狀態,並給咱們帶來麻煩。若是咱們不能構造一個無效的狀態,咱們的程序就很難以無效的狀態結束,由於爲了達到無效的狀態,程序必須遵循一系列構造無效狀態的操做。可是這樣的程序在類型級別上是無效的,排版檢查程序會告訴咱們:咱們作了一些錯誤的事情。這很棒,因爲類型系統會爲咱們記住數據的約束條件,因此咱們沒必要依賴愛忘事的內存來記住它們。前端

      幸運的是,這項技術的許多結果能夠應用於其餘編程語言,今天咱們將在 Typescript 中進行試驗。android

一個例子

       讓咱們來研究一個示例問題,這樣咱們就能夠嘗試理解如何使用它。咱們將使用代數數據類型來約束一個函數的類型,這樣咱們就能夠防止對它使用無效參數。咱們的小例子以下:ios

  • 咱們有一個接受單個參數的函數:一個對象有兩個字段,稱爲 field1field2
  • 對象不能同時具備這兩個字段。
  • 對象可能只有 field1,沒有 field2
  • 只有當對象有 field1 時,它纔能有 field2
  • 所以,具備 field2 的對象而沒有 field1 的對象無效。
  • 爲簡單起見,當存在 field1field2 時,它們將是 string 類型,但它們自己能夠是任何類型的。

缺少經驗的解決方案

      讓咱們從最簡單的方法開始。因爲 field1field2 均可以存在,或者不存在,因此咱們只是讓它們成爲可選的。git

interface Fields {
  field1?: string;
  field2?: string;
};

function receiver(f: Fields) {
  if (f.field1 === undefined && f.field2 !== undefined) {
    throw new Error("Oh noes, this should be impossible!");
  }
  // 其餘邏輯代碼
}複製代碼

不幸的是,這並不能阻止編譯時的任何操做,還須要在運行時檢查可能的錯誤。github

// 這不會在編譯時引起任何錯誤
// 因此咱們必須在運行時發現它
receiver({field2: "Hahaha, I didn't put a field1!"})複製代碼

基本 ADT 解決方案

       因此咱們連續幾回在一行中用錯誤的字段調用 receiver,咱們的應用程序就會出問題。我麼彷佛該作些什麼了。讓咱們再看一下這些示例,以便咱們能夠查看是否能夠生成正確的類型:typescript

  • 對象不能同時具備這兩個字段。
  • 對象只能有 field1,不能有 field2
  • 只有當對象有 field1 時,它纔能有 field2。所以,在本例中,對象同時具備 field1field2
  • 具備 field2 的對象無效,而不是具備 field1 的對象。

讓咱們把它記錄成這種類型:編程

interface NoFields {};

interface Field1Only {
  field1: string;
};

interface BothField1AndField2 {
  field1: string;
  field2: string;
};

interface InvalidObject {
  field2: string;
};複製代碼

       咱們這裏也包括 InvalidObject,可是寫它有點傻,由於咱們不但願它真的存在。咱們能夠將其做爲文檔保存,或者刪除它,以便進一步確認它不該該存在。如今讓咱們爲 Fields 字段編寫一個類型:後端

type Fields = NoFields | Field1Only | BothField1AndField2;  // 我故意把放在這裏的無效對象忘了複製代碼

有了這種處理方式,就很難將 InvalidObject 發送給 receiver安全

receiver({field2: "Hahaha, I didn't put a field1!"});  // 類型錯誤!這個對象和 `Fields` 不匹配複製代碼

       咱們還須要稍微調整一下 receiver 函數,主要是由於字段如今可能不存在,排版檢查程序如今須要證實你將要讀取的字段是否實際存在:bash

function receiver(f: Fields) {
  if ("field1" in f) {
    if ("field2" in f) {
      // 爲 f.field1 和 f.field2 作些操做
    } else {
      // 爲 f.field1 作些操做, 但 f.field2 不存在
    }
  } else {
    // f 是個空字段
  }
}複製代碼

結構類型的限制

      不幸的是,不管好壞, Typescript 都是一個結構類型系統,若是咱們不當心的話,它會容許咱們繞過一些安全問題。 Typescript 中的 NoFields 類型(空對象、{})。在 Typescript 中,這意味着與咱們但願它作的徹底不一樣的事情。實際上,當咱們寫的時候,它是這樣:

interface Foo {
  field: string;
};複製代碼

      Typescript 會理解任何帶有 field ,類型爲 stringobject 都是可行的,除了咱們建立新對象的狀況,例如:

const myFoo : Foo = { field: "asdf" };  // 在這種狀況下,咱們沒法添加更多字段複製代碼

      可是,在賦值時,將 Typescript 測試用作類型腳本,這意味着咱們的對象,可能會以咱們但願它們具備的更多字段結束:

const getReady = { field: "asdf", unexpectedField: "hehehe" };
const myFoo : Foo = getReady;  // 這不是一個錯誤複製代碼

      所以,當咱們將這個想法擴展到空對象 {} 時,發如今賦值時,只要該值是一個對象,而且具備所需的全部字段, Typescript 就會接受任何值。由於類型不須要字段,因此第二個條件對於任何 object 都很是成功,這徹底不是咱們想要它作的。

禁止意外字段

       讓咱們試着爲沒有字段的對象建立一個類型,這樣咱們實際上就不得不用本身的方法來愚弄類型檢查器。咱們已經知道 never,這是一種永遠沒法知足的類型。如今咱們須要另外一種成分來表示「每個可能的領域」。這個成分是:[鍵:字符串]:類型。有了這兩個,咱們就能夠在沒有字段的狀況下構造對象。

type NoFields = {
  [key: string]: never;
};複製代碼

      此類型表示:這是一個對象,其字段類型爲 never。因爲不能構造 never,沒法爲此對象的字段生成有效值。因此,惟一的解決方案是建立一個沒有字段的對象。如今,咱們必須更加謹慎地打破這些類型:

type NoFields = {
  [key: string]: never;
};

interface Field1Only {
  field1: string;
};

interface BothField1AndField2 {
  field1: string;
  field2: string;
};

type Fields = NoFields | Field1Only | BothField1AndField2;

const broken = {field2: "asdf"};

// Bypass1: 遍歷空對象類型
// Empty object is a well known code smell in Typescript
const bypass1 : {} = broken;
const brokenThroughBypass1 : Fields = bypass1;

// Bypass2: 使用 `any` 轉移 hatch
// any 在 Typescript 是另外一個有名的代碼 
const bypass2 : any = broken;
const brokenThroughBypass2 : Fields = bypass2;複製代碼

       如今看來,咱們須要兩個很是具體的步驟來破壞這個系統,這確定是很是困難的。若是咱們必須深刻地構建一個程序,咱們應該注意到一些問題。

結論

      今天,咱們看到了一種經過類型保證程序正確性的方法,它應用於一種更主流的語言:Typescript。雖然 Typescript 不能保證與 Haskell 相同的安全級別,但這並不妨礙咱們將 Haskell 的一些想法應用於 Typescript。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索