TypeScript 之常見型別(下)

冴羽發表於2021-12-09

TypeScript 的官方文件早已更新,但我能找到的中文文件都還停留在比較老的版本。所以對其中新增以及修訂較多的一些章節進行了翻譯整理。

本篇翻譯整理自 TypeScript Handbook 中 「Everyday Types」 章節。

本文並不嚴格按照原文翻譯,對部分內容也做了解釋補充。

型別別名(Type Aliases)

我們已經學會在型別註解裡直接使用物件型別和聯合型別,這很方便,但有的時候,一個型別會被使用多次,此時我們更希望通過一個單獨的名字來引用它。

這就是型別別名(type alias)。所謂型別別名,顧名思義,一個可以指代任意型別的名字。型別別名的語法是:

type Point = {
  x: number;
  y: number;
};
 
// Exactly the same as the earlier example
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

你可以使用型別別名給任意型別一個名字,舉個例子,命名一個聯合型別:

type ID = number | string;

注意別名是唯一的別名,你不能使用型別別名建立同一個型別的不同版本。當你使用型別別名的時候,它就跟你編寫的型別是一樣的。換句話說,程式碼看起來可能不合法,但對 TypeScript 依然是合法的,因為兩個型別都是同一個型別的別名:

type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}
 
// Create a sanitized input
let userInput = sanitizeInput(getInput());
 
// Can still be re-assigned with a string though
userInput = "new input";

介面(Interfaces)

介面宣告(interface declaration)是命名物件型別的另一種方式:

interface Point {
  x: number;
  y: number;
}
 
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });

就像我們在上節使用的型別別名,這個例子也同樣可以執行,就跟我們使用了一個匿名物件型別一樣。TypeScript 只關心傳遞給 printCoord 的值的結構(structure)——關心值是否有期望的屬性。正是這種只關心型別的結構和能力的特性,我們才認為 TypeScript 是一個結構化(structurally)的型別系統。

型別別名和介面的不同

型別別名和介面非常相似,大部分時候,你可以任意選擇使用。介面的幾乎所有特性都可以在 type 中使用,兩者最關鍵的差別在於型別別名本身無法新增新的屬性,而介面是可以擴充套件的。

// Interface
// 通過繼承擴充套件型別
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey
        
// Type
// 通過交集擴充套件型別
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear = getBear();
bear.name;
bear.honey;
// Interface
// 對一個已經存在的介面新增新的欄位
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
        
// Type
// 建立後不能被改變
type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

// Error: Duplicate identifier 'Window'.

在後續的章節裡,你還會了解的更多。所以下面這些內容不能立刻理解也沒有關係:

大部分時候,你可以根據個人喜好進行選擇。TypeScript 會告訴你它是否需要其他方式的宣告。如果你喜歡探索性的使用,那就使用 interface ,直到你需要用到 type 的特性。

型別斷言(Type Assertions)

有的時候,你知道一個值的型別,但 TypeScript 不知道。

舉個例子,如果你使用 document.getElementById,TypeScript 僅僅知道它會返回一個 HTMLElement,但是你卻知道,你要獲取的是一個 HTMLCanvasElement

這時,你可以使用型別斷言將其指定為一個更具體的型別:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

就像型別註解一樣,型別斷言也會被編譯器移除,並且不會影響任何執行時的行為。

你也可以使用尖括號語法(注意不能在 .tsx 檔案內使用),是等價的:

const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
謹記:因為型別斷言會在編譯的時候被移除,所以執行時並不會有型別斷言的檢查,即使型別斷言是錯誤的,也不會有異常或者 null 產生。

TypeScript 僅僅允許型別斷言轉換為一個更加具體或者更不具體的型別。這個規則可以阻止一些不可能的強制型別轉換,比如:

const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

有的時候,這條規則會顯得非常保守,阻止了你原本有效的型別轉換。如果發生了這種事情,你可以使用雙重斷言,先斷言為 any (或者是 unknown),然後再斷言為期望的型別:

const a = (expr as any) as T;

字面量型別(Literal Types)

除了常見的型別 stringnumber ,我們也可以將型別宣告為更具體的數字或者字串。

眾所周知,在 JavaScript 中,有多種方式可以宣告變數。比如 varlet ,這種方式宣告的變數後續可以被修改,還有 const ,這種方式宣告的變數則不能被修改,這就會影響 TypeScript 為字面量建立型別。

let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
// let changingString: string
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
// const constantString: "Hello World"

字面量型別本身並沒有什麼太大用:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.

如果結合聯合型別,就顯得有用多了。舉個例子,當函式只能傳入一些固定的字串時:

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.

數字字面量型別也是一樣的:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

當然了,你也可以跟非字面量型別聯合:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");

// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

還有一種字面量型別,布林字面量。因為只有兩種布林字面量型別, truefalse ,型別 boolean 實際上就是聯合型別 true | false 的別名。

字面量推斷(Literal Inference)

當你初始化變數為一個物件的時候,TypeScript 會假設這個物件的屬性的值未來會被修改,舉個例子,如果你寫下這樣的程式碼:

const obj = { counter: 0 };
if (someCondition) {
  obj.counter = 1;
}

TypeScript 並不會認為 obj.counter 之前是 0, 現在被賦值為 1 是一個錯誤。換句話說,obj.counter 必須是 string 型別,但不要求一定是 0,因為型別可以決定讀寫行為。

這也同樣應用於字串:

declare function handleRequest(url: string, method: "GET" | "POST"): void;

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);

// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

在上面這個例子裡,req.method 被推斷為 string ,而不是 "GET",因為在建立 req 和 呼叫 handleRequest 函式之間,可能還有其他的程式碼,或許會將 req.method 賦值一個新字串比如 "Guess" 。所以 TypeScript 就報錯了。

有兩種方式可以解決:

  1. 新增一個型別斷言改變推斷結果:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

修改 1 表示“我有意讓 req.method 的型別為字面量型別 "GET",這會阻止未來可能賦值為 "GUESS" 等欄位”。修改 2 表示“我知道 req.method 的值是 "GET"”.

  1. 你也可以使用 as const 把整個物件轉為一個型別字面量:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);

as const 效果跟 const 類似,但是對型別系統而言,它可以確保所有的屬性都被賦予一個字面量型別,而不是一個更通用的型別比如 string 或者 number

nullundefined

JavaScript 有兩個原始型別的值,用於表示空缺或者未初始化,他們分別是 nullundefined

TypeScript 有兩個對應的同名型別。它們的行為取決於是否開啟了 strictNullChecks 選項。

strictNullChecks 關閉

strictNullChecks 選項關閉的時候,如果一個值可能是 null 或者 undefined,它依然可以被正確的訪問,或者被賦值給任意型別的屬性。這有點類似於沒有空值檢查的語言 (比如 C# ,Java) 。這些檢查的缺少,是導致 bug 的主要源頭,所以我們始終推薦開發者開啟 strictNullChecks 選項。

strictNullChecks 開啟

strictNullChecks 選項開啟的時候,如果一個值可能是 null 或者 undefined,你需要在用它的方法或者屬性之前,先檢查這些值,就像用可選的屬性之前,先檢查一下 是否是 undefined ,我們也可以使用型別收窄(narrowing)檢查值是否是 null

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

非空斷言操作符(字尾 !)(Non-null Assertion Operator)

TypeScript 提供了一個特殊的語法,可以在不做任何檢查的情況下,從型別中移除 nullundefined,這就是在任意表示式後面寫上 ! ,這是一個有效的型別斷言,表示它的值不可能是 null 或者 undefined

function liveDangerously(x?: number | null) {
  // No error
  console.log(x!.toFixed());
}

就像其他的型別斷言,這也不會更改任何執行時的行為。重要的事情說一遍,只有當你明確的知道這個值不可能是 null 或者 undefined 時才使用 !

列舉(Enums)

列舉是 TypeScript 新增的新特性,用於描述一個值可能是多個常量中的一個。不同於大部分的 TypeScript 特性,這並不是一個型別層面的增量,而是會新增到語言和執行時。因為如此,你應該瞭解下這個特性。但是可以等一等再用,除非你確定要使用它。你可以在列舉型別頁面瞭解更多的資訊。

不常見的原始型別(Less Common Primitives)

我們提一下在 JavaScript 中剩餘的一些原始型別。但是我們並不會深入講解。

bigInt

ES2020 引入原始型別 BigInt,用於表示非常大的整數:

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
 
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

你可以在 TypeScript 3.2 的釋出日誌中瞭解更多資訊。

symbol

這也是 JavaScript 中的一個原始型別,通過函式 Symbol(),我們可以建立一個全域性唯一的引用:

const firstName = Symbol("name");
const secondName = Symbol("name");
 
if (firstName === secondName) {
  // This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
  // Can't ever happen
}

你可以在 Symbol 頁面瞭解更多的資訊。

TypeScript 系列

  1. TypeScript 之 基礎入門
  2. TypeScript 之 常見型別(上)
  3. TypeScript 之 型別收窄
  4. TypeScript 之 函式
  5. TypeScript 之 物件型別
  6. TypeScript 之 泛型
  7. TypeScript 之 Keyof 操作符
  8. TypeScript 之 Typeof 操作符
  9. TypeScript 之 索引訪問型別
  10. TypeScript 之 條件型別

微信:「mqyqingfeng」,加我進冴羽唯一的讀者群。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章