TypeScript 4.1 新特性

最近更新時間 2020-12-04 14:07:46

模板字面量類型(Template Literal Types)

TypeScript 允許字符串字面量類型變量在函數或 APIS 中指定一組特定的字符串。

function setVerticalAlignment(color: "top" | "middle" | "bottom") {
  // ...
}

setVerticalAlignment("middel");
Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.

這非常好,因為字符串字面量類型可以對我們的輸入參數進行拼寫檢查。

我們還可以將字符串字面量類型用作映射類型中的屬性名稱。這種特性,可用作構建屬性定義,簡化代碼。

type Options = {
  [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

但是在另一個地方,字符串字面量類型可以用作構建塊:構建其他字符串字面量類型。

這就是為什麼 TypeScript 4.1 引入模板字符串字面量類型的原因。它具有與 JavaScript 中的模板文字字面量相同的語法,但用於類型位置。當將它與具體字面量類型一起使用時,它將通過串聯內容來產生新的字符串字面量類型。

type World = "world";

type Greeting = `hello ${World}`;
//   ^ = type Greeting = "hello world"

當聯合類型在子字符串中替換時會發生什麼呢?它產生可以由每個聯合成員表示的每個可能的字符串字面量的集合。以下兩個變量運行出現四種可能的值。

type Color = "red" | "blue";
type Quantity = "one" | "two";

type SeussFish = `${Quantity | Color} fish`;
//   ^ = type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"

除發行說明中的​​示例外,還可以這樣使用此功能。例如,用於 UI 組件的多個庫提供了一種在其 API 中同時指定垂直和水平對齊方式的方法,通常同時使用單個字符串(例如“右下”)同時指定兩者。在垂直對齊 “top”,“middle” 和 “ bottom” 以及水平對齊 “left”,“center” 和 “right” 之間,存在9種可能的字符串,其中每個前一個字符串與每個後面的字符串使用破折號。

type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";

// Takes
//   | "top-left"    | "top-center"    | "top-right"
//   | "middle-left" | "middle-center" | "middle-right"
//   | "bottom-left" | "bottom-center" | "bottom-right"

declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;

setAlignment("top-left");   // works!
setAlignment("top-middel"); // error!
Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
setAlignment("top-pot");    // error! but good doughnuts if you're ever in Seattle
Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.

儘管有很多此類 API 的示例,我們可以手動將其編寫出來,這只是一個簡單示例。實際上,對於9個字符串的示例相對比較簡單。但是,當您需要大量字符串時,最好提前自動生成它們,以節省每次類型檢查的工作(使用字符串,這將更容易理解)。

一些實際價值來自動態創建新的字符串文字。例如,有一個 makeWatchedObject API,它接受一個對象並克隆相同的對象,有一個 on 方法來檢測屬性的變化。

let person = makeWatchedObject({
  firstName: "Homer",
  age: 42, // give-or-take
  location: "Springfield",
});

person.on("firstNameChanged", () => {
  console.log(`firstName was changed!`);
});

on 偵聽事件 “firstNameChanged”,而不僅僅是 “firstName”。我們將如何輸入?

type PropEventSource = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};

/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject(obj: T): T & PropEventSource;

這樣,當我們賦予錯誤的屬性時,我們可以構建出錯誤的東西!

// error!
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
// error!
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.

我們還可以用模板文字字面量做一些特殊的事情:我們可以從替換位置推斷出來。下面示例從 eventName 字符串的各個部分進行推斷,以找出關聯的屬性。

type PropEventSource = {
    on
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};

declare function makeWatchedObject(obj: T): T & PropEventSource;

let person = makeWatchedObject({
    firstName: "Homer",
    age: 42,
    location: "Springfield",
});

// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
    // 'newName' has the type of 'firstName'
    console.log(`new name is ${newName.toUpperCase()}`);
});

// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
    if (newAge < 0) {
        console.log("warning! negative age");
    }
})

在這裡,我們介紹了一種通用方法。當使用字符串 “firstNameChanged” 進行調用時,TypeScript 會嘗試推斷 K 的正確類型。為此,它將 K 與 “Changed” 之前的內容進行匹配,並推斷字符串 “firstName”。on 方法可以獲取原始對象上的 firstName 類型,在這種情況下為字符串。類似地,當我們使用 “ageChanged” 調用時,它會找到屬性 age 的類型(即number)。

推理可以以不同的方式組合,通常是對字符串進行解構,並以不同的方式對其進行重構。實際上,為了幫助修改這些字符串文字類型,我們添加了一些新的實用程序類型別名,用於修改字母中的大小寫(即轉換為小寫和大寫字符)。

type EnthusiasticGreeting = `${Uppercase}`

type HELLO = EnthusiasticGreeting<"hello">;
//   ^ = type HELLO = "HELLO"

新的類型別名為 Uppercase, Lowercase, Capitalize and Uncapitalize。前兩個轉換字符串中的每個字符,後兩個僅轉換字符串中的第一個字符。

映射類型中的鍵重映射(Key Remapping in Mapped Types)

映射類型可以基於任意鍵創建新的對象類型。

type Options = {
  [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

新的方法,基於其他對象類型的新對象類型。

/// 'Partial' is the same as 'T', but with each property marked optional.
type Partial = {
  [K in keyof T]?: T[K];
};

到目前為止,映射類型只能使用您提供的鍵來產生新的對象類型。但是,很多時候您希望能夠根據輸入來創建新鍵或過濾掉鍵。

因此,TypeScript 4.1允許您使用新的as子句重新映射映射類型中的鍵。

type MappedTypeWithNewKeys = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            This is the new syntax!
}

使用這個新的as子句,您可以利用模板字面量類型之類的功能輕鬆地基於舊名稱創建屬性名稱。

type Getters = {
    [K in keyof T as `get${Capitalize}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters;
//   ^ = type LazyPerson = {
//       getName: () => string;
//       getAge: () => number;
//       getLocation: () => string;
//   }

可以通過 never 過濾 keys。這意味著在某些情況下,您不必使用額外的 Omit 程序類型。

// Remove the 'kind' property
type RemoveKindField = {
    [K in keyof T as Exclude]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField;
//   ^ = type KindlessCircle = {
//       radius: number;
//   }

遞歸條件類型(Recursive Conditional Types)

例如,如果我們想編寫一個類型來獲取嵌套數組的元素類型,則可以編寫以下deepFlatten類型。

type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;

function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
  throw "not implemented";
}

// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

類似地,在 TypeScript 4.1 中,我們可以編寫一個 Awaited 類型來深度解包 Promises。

type Awaited<T> = T extends PromiseLike ? Awaited<U> : T;

/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
  p: Promise<T>,
  onFulfilled: (value: Awaited<T>) => U
): Promise>;

請記住,儘管這些遞歸類型功能強大,但應負責任地謹慎使用。

首先,這些類型可以完成很多工作,這意味著它們可以增加類型檢查時間。嘗試以Collat​​z猜想或斐波那契數列為模型建模可能很有趣,但不要將其放在npm的.d.ts文件中。

但是,除了計算量大之外,這些類型還可能在足夠複雜的輸入上達到內部遞歸深度限制。當達到該遞歸限制時,將導致編譯時錯誤。通常,最好不要使用這些類型,而要寫一些在更實際的示例中失敗的東西。

rss_feed