- 原文地址:Using Typescript to make invalid states irrepresentable
- 原文做者:Javier Casas
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:solerji
- 校對者:lgh757079506,lsvih
有一種好的 Haskell 編程原則,一樣也是一種好的函數式編程原則,叫作使無效狀態不可恢復原則。這是什麼原則呢?一般咱們使用類型系統來構建對數據和狀態施加約束的類型,從而達到能夠表明已存在狀態的效果。如今,在類型級別上,咱們設法消除了無效狀態,但類型系統每次試圖構造無效狀態時都會介入無效狀態,並給咱們帶來麻煩。若是咱們不能構造一個無效的狀態,咱們的程序就很難以無效的狀態結束,由於爲了達到無效的狀態,程序必須遵循一系列構造無效狀態的操做。可是這樣的程序在類型級別上是無效的,排版檢查程序會告訴咱們:咱們作了一些錯誤的事情。這很棒,因爲類型系統會爲咱們記住數據的約束條件,因此咱們沒必要依賴愛忘事的內存來記住它們。前端
幸運的是,這項技術的許多結果能夠應用於其餘編程語言,今天咱們將在 Typescript 中進行試驗。android
讓咱們來研究一個示例問題,這樣咱們就能夠嘗試理解如何使用它。咱們將使用代數數據類型來約束一個函數的類型,這樣咱們就能夠防止對它使用無效參數。咱們的小例子以下:ios
field1
和 field2
。field1
,沒有 field2
。field1
時,它纔能有 field2
。field2
的對象而沒有 field1
的對象無效。field1
或 field2
時,它們將是 string
類型,但它們自己能夠是任何類型的。 讓咱們從最簡單的方法開始。因爲 field1
和 field2
均可以存在,或者不存在,因此咱們只是讓它們成爲可選的。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!"})複製代碼
因此咱們連續幾回在一行中用錯誤的字段調用 receiver
,咱們的應用程序就會出問題。我麼彷佛該作些什麼了。讓咱們再看一下這些示例,以便咱們能夠查看是否能夠生成正確的類型:typescript
field1
,不能有 field2
。field1
時,它纔能有 field2
。所以,在本例中,對象同時具備 field1
和 field2
。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
,類型爲 string
的 object
都是可行的,除了咱們建立新對象的狀況,例如:
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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。