[譯] TypeScript 3.0: unknown 類型

TypeScript 3.0 引入了新的unknown 類型,它是 any 類型對應的安全類型。前端

unknownany 的主要區別是 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);
}
複製代碼

除了使用 typeofinstanceof 運算符以外,咱們還可使用自定義類型保護函數縮小 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 類型使用類型斷言

在上一節中,咱們已經看到如何使用 typeofinstanceof 和自定義類型保護函數來講服 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
複製代碼

讓咱們回顧一下 IntersectionType3unknown & string 類型表示全部能夠被同時賦值給 unknownstring 類型的值。因爲每種類型均可以賦值給 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 是一個被標記的聯合類型。在其它語言中,它也能夠被稱做 MaybeOption 或者 Optional。咱們使用 Result 來清楚地模擬操做的成功和不成功的結果。

tryDeserializeLocalStorageItem 的函數調用者在嘗試使用 valueerror 屬性以前必須首先檢查 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 來表示反序列化失敗,緣由以下:

  1. null 值是一個有效的 JSON 值。所以,咱們沒法區分是對值 null 進行了反序列化,仍是因爲缺乏參數或語法錯誤而致使整個操做失敗。
  2. 若是咱們從函數返回 null,咱們沒法同時返回錯誤。所以,咱們函數的調用者不知道操做失敗的緣由。

爲了完整性,這種方法的更成熟的替代方案是使用類型解碼器進行安全的 JSON 解析。解碼器須要咱們指定要反序列化的值的預期數據結構。若是持久化的JSON結果與該數據結構不匹配,則解碼將以明肯定義的方式失敗。這樣,咱們的函數老是返回有效或失敗的解碼結果,就再也不須要 unknown 類型了。

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


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

相關文章
相關標籤/搜索