- 原文地址:TypeScript 3.0: The unknown Type
- 原文做者:Marius Schulz
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:shixi-li
- 校對者:Usey95, smilemuffie
TypeScript 3.0 引入了新的unknown
類型,它是 any
類型對應的安全類型。前端
unknown
和 any
的主要區別是 unknown
類型會更加嚴格:在對 unknown
類型的值執行大多數操做以前,咱們必須進行某種形式的檢查。而在對 any
類型的值執行操做以前,咱們沒必要進行任何檢查。android
這片文章主要關注於 unknown
類型的實際應用,以及包含了與 any
類型的比較。若是須要更全面的代碼示例來了解 unknown
類型的語義,能夠看看 Anders Hejlsberg 的原始拉取請求。ios
any
類型讓咱們首先看看 any
類型,這樣咱們就能夠更好地理解引入 unknown
類型背後的動機。git
自從 TypeScript 在 2012 年發佈第一個版本以來 any
類型就一直存在。它表明全部可能的 JavaScript 值 — 基本類型,對象,數組,函數,Error,Symbol,以及任何你可能定義的值。github
在 TypeScript 中,任何類型均可以被歸爲 any 類型。這讓 any
類型成爲了類型系統的 頂級類型 (也被稱做 全局超級類型)。typescript
這是一些咱們賦值給 any
類型的代碼示例:json
let value: any;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
複製代碼
any
類型本質上是類型系統的一個逃逸艙。做爲開發者,這給了咱們很大的自由:TypeScript容許咱們對 any
類型的值執行任何操做,而無需事先執行任何形式的檢查。後端
在上述例子中,變量 value
被定義成類型 any
。也是所以,TypeScript 認爲如下全部操做都是類型正確的:數組
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
複製代碼
這許多場景下,這樣的機制都太寬鬆了。使用any
類型,能夠很容易地編寫類型正確可是執行異常的代碼。若是咱們使用 any
類型,就沒法享受 TypeScript 大量的保護機制。安全
但若是能有頂級類型也能默認保持安全呢?這就是 unknown
到來的緣由。
unknown
類型就像全部類型均可以被歸爲 any
,全部類型也均可以被歸爲 unknown
。這使得 unknown
成爲 TypeScript 類型系統的另外一種頂級類型(另外一種是 any
)。
這是咱們以前看到的相同的一組賦值示例,此次使用類型爲 unknown
的變量:
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
複製代碼
對 value
變量的全部賦值都被認爲是類型正確的。
當咱們嘗試將類型爲 unknown
的值賦值給其餘類型的變量時會發生什麼?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
複製代碼
unknown
類型只能被賦值給 any
類型和 unknown
類型自己。直觀的說,這是有道理的:只有可以保存任意類型值的容器才能保存 unknown
類型的值。畢竟咱們不知道變量 value
中存儲了什麼類型的值。
如今讓咱們看看當咱們嘗試對類型爲 unknown
的值執行操做時會發生什麼。如下是咱們以前看過的相同操做:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
複製代碼
將 value
變量類型設置爲 unknown
後,這些操做都再也不被認爲是類型正確的。經過改變 any
類型到 unknown
類型,咱們的默認設置從容許一切翻轉式的改變成了幾乎什麼都不容許。
這是 unknown
類型的主要價值主張:TypeScript 不容許咱們對類型爲 unknown
的值執行任意操做。相反,咱們必須首先執行某種類型檢查以縮小咱們正在使用的值的類型範圍。
unknown
類型範圍咱們能夠經過不一樣的方式將 unknown
類型縮小爲更具體的類型範圍,包括 typeof
運算符,instanceof
運算符和自定義類型保護函數。全部這些縮小類型範圍的技術都有助於 TypeScript 的基於控制流的類型分析。
如下示例說明了 value
如何在兩個 if
語句分支中得到更具體的類型:
function stringifyForLogging(value: unknown): string {
if (typeof value === "function") {
// Within this branch, `value` has type `Function`,
// so we can access the function's `name` property
const functionName = value.name || "(anonymous)";
return `[function ${functionName}]`;
}
if (value instanceof Date) {
// Within this branch, `value` has type `Date`,
// so we can call the `toISOString` method
return value.toISOString();
}
return String(value);
}
複製代碼
除了使用 typeof
或 instanceof
運算符以外,咱們還可使用自定義類型保護函數縮小 unknown
類型範圍:
/** * A custom type guard function that determines whether * `value` is an array that only contains numbers. */
function isNumberArray(value: unknown): value is number[] {
return (
Array.isArray(value) &&
value.every(element => typeof element === "number")
);
}
const unknownValue: unknown = [15, 23, 8, 4, 42, 16];
if (isNumberArray(unknownValue)) {
// Within this branch, `unknownValue` has type `number[]`,
// so we can spread the numbers as arguments to `Math.max`
const max = Math.max(...unknownValue);
console.log(max);
}
複製代碼
儘管 unknownValue
已經被歸爲 unknown
類型,請注意它如何依然在 if 分支下獲取到 number[]
類型。
unknown
類型使用類型斷言在上一節中,咱們已經看到如何使用 typeof
,instanceof
和自定義類型保護函數來講服 TypeScript 編譯器某個值具備某種類型。這是將 「unknown」 類型指定爲更具體類型的安全且推薦的方法。
若是要強制編譯器信任類型爲 unknown
的值爲給定類型,則可使用相似這樣的類型斷言:
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"
複製代碼
請注意,TypeScript 事實上未執行任何特殊檢查以確保類型斷言實際上有效。類型檢查器假定你更瞭解並相信你在類型斷言中使用的任何類型都是正確的。
若是你犯了錯誤並指定了錯誤的類型,這很容易致使在運行時拋出錯誤:
const value: unknown = 42;
const someString: string = value as string;
const otherString = someString.toUpperCase(); // BOOM
複製代碼
這個 value
變量值是一個數字, 但咱們假設它是一個字符串並使用類型斷言 value as string
。因此請謹慎使用類型斷言!
unknown
類型如今讓咱們看一下在聯合類型中如何處理 unknown
類型。在下一節中,咱們還將瞭解交叉類型。
在聯合類型中,unknown
類型會吸取任何類型。這就意味着若是任一組成類型是 unknown
,聯合類型也會至關於 unknown
:
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
複製代碼
這條規則的一個意外是 any
類型。若是至少一種組成類型是 any
,聯合類型會至關於 any
:
type UnionType5 = unknown | any; // any
複製代碼
因此爲何 unknown
能夠吸取任何類型(any
類型除外)?讓咱們來想一想 unknown | string
這個例子。這個類型能夠表示任何 unkown 類型或者 string 類型的值。就像咱們以前瞭解到的,全部類型的值均可以被定義爲 unknown
類型,其中也包括了全部的 string
類型,所以,unknown | string
就是表示和 unknown
類型自己相同的值集。所以,編譯器能夠將聯合類型簡化爲 unknown
類型。
unknown
類型在交叉類型中,任何類型均可以吸取 unknown
類型。這意味着將任何類型與 unknown
相交不會改變結果類型:
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
複製代碼
讓咱們回顧一下 IntersectionType3
:unknown & string
類型表示全部能夠被同時賦值給 unknown
和 string
類型的值。因爲每種類型均可以賦值給 unknown
類型,因此在交叉類型中包含 unknown
不會改變結果。咱們將只剩下 string
類型。
unknown
的值的運算符unknown
類型的值不能用做大多數運算符的操做數。這是由於若是咱們不知道咱們正在使用的值的類型,大多數運算符不太可能產生有意義的結果。
你能夠在類型爲 unknown
的值上使用的運算符只有四個相等和不等運算符:
===
==
!==
!=
若是要對類型爲 unknown
的值使用任何其餘運算符,則必須先指定類型(或使用類型斷言強制編譯器信任你)。
localStorage
中讀取JSON這是咱們如何使用 unknown
類型的真實例子。
假設咱們要編寫一個從 localStorage
讀取值並將其反序列化爲 JSON 的函數。若是該項不存在或者是無效 JSON,則該函數應返回錯誤結果,不然,它應該反序列化並返回值。
由於咱們不知道在反序列化持久化的 JSON 字符串後咱們會獲得什麼類型的值。咱們將使用 unknown
做爲反序列化值的類型。這意味着咱們函數的調用者必須在對返回值執行操做以前進行某種形式的檢查(或者使用類型斷言)。
這裏展現了咱們怎麼實現這個函數:
type Result =
| { success: true, value: unknown }
| { success: false, error: Error };
function tryDeserializeLocalStorageItem(key: string): Result {
const item = localStorage.getItem(key);
if (item === null) {
// The item does not exist, thus return an error result
return {
success: false,
error: new Error(`Item with key "${key}" does not exist`)
};
}
let value: unknown;
try {
value = JSON.parse(item);
} catch (error) {
// The item is not valid JSON, thus return an error result
return {
success: false,
error
};
}
// Everything's fine, thus return a success result
return {
success: true,
value
};
}
複製代碼
返回值類型 Result
是一個被標記的聯合類型。在其它語言中,它也能夠被稱做 Maybe
、Option
或者 Optional
。咱們使用 Result
來清楚地模擬操做的成功和不成功的結果。
tryDeserializeLocalStorageItem
的函數調用者在嘗試使用 value
或 error
屬性以前必須首先檢查 success
屬性:
const result = tryDeserializeLocalStorageItem("dark_mode");
if (result.success) {
// We've narrowed the `success` property to `true`,
// so we can access the `value` property
const darkModeEnabled: unknown = result.value;
if (typeof darkModeEnabled === "boolean") {
// We've narrowed the `unknown` type to `boolean`,
// so we can safely use `darkModeEnabled` as a boolean
console.log("Dark mode enabled: " + darkModeEnabled);
}
} else {
// We've narrowed the `success` property to `false`,
// so we can access the `error` property
console.error(result.error);
}
複製代碼
請注意,tryDeserializeLocalStorageItem
函數不能簡單地經過返回 null
來表示反序列化失敗,緣由以下:
null
值是一個有效的 JSON 值。所以,咱們沒法區分是對值 null
進行了反序列化,仍是因爲缺乏參數或語法錯誤而致使整個操做失敗。null
,咱們沒法同時返回錯誤。所以,咱們函數的調用者不知道操做失敗的緣由。爲了完整性,這種方法的更成熟的替代方案是使用類型解碼器進行安全的 JSON 解析。解碼器須要咱們指定要反序列化的值的預期數據結構。若是持久化的JSON結果與該數據結構不匹配,則解碼將以明肯定義的方式失敗。這樣,咱們的函數老是返回有效或失敗的解碼結果,就再也不須要 unknown
類型了。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。