用了模板字面量類型,同事直呼太強了!
你了解過模板字面量類型么?你想知道如何利用模板字面量類型,來減少 TypeScript 項目中的重復代碼么?如果想的話,閱讀完本文內容之后,也許你就懂了。
假設我們想要定義一種類型來描述 CSS padding 規(guī)則,如果你了解 TypeScript 類型別名和聯(lián)合類型的話,能很容易定義出 CssPadding 類型。
type CssPadding =
| "padding-left"
| "padding-right"
| "padding-top"
| "padding-bottom";
但如果我們想要繼續(xù)定義一種新的類型來描述 CSS margin 規(guī)則,你是不是立馬想到與定義 CssPadding 類型一樣的方式。
type MarginPadding =
| "margin-left"
| "margin-right"
| "margin-top"
| "margin-bottom";
對于以上定義的兩種類型來說,雖然它們都能滿足我們的需求。但在定義這兩種類型的過程中,仍然存在一些重復的代碼。
那么如何解決這個問題呢?這時我們可以使用 TypeScript 4.1 版本引入了新的模板字面量類型,具體的使用方式如下:
type Direction = "left" | "right" | "top" | "bottom";
type CssPadding = `padding-$ Direction `;
type MarginPadding = `margin-$ Direction `;
看完以上代碼,是不是覺得簡潔很多。與 JavaScript 中的模板字符串類似,模板字面量類型被括在反引號中,同時可以包含 ${T}? 形式的占位符,其中類型變量 T 的類型可以是 string、number、boolean? 或 bigint 類型。
模板字面量類型不僅為我們提供了連接字符串字面量的能力,而且還可以把非字符串基本類型的字面量轉換為對應的字符串字面量類型。下面我們來舉一些具體的例子:
type EventName<T extends string> = `$T Changed`;
type Concat<S1 extends string, S2 extends string> = `$ S1 -$ S2 `;
type ToString<T extends string | number | boolean | bigint> = `$ T `;
type T0 = EventName<"foo">; // 'fooChanged'
type T1 = Concat<"Hello", "World">; // 'Hello-World'
type T2 = ToString<"阿寶哥" | 666 | true | -1234n>; // "阿寶哥" | "true" | "666" | "-1234"
對于上述的例子來說,其實并不復雜。但現(xiàn)在問題來了,如果傳入 EventName 或 Concat 工具類型的實際類型是聯(lián)合類型的話,那么結果又會是怎樣呢?接下來,我們來驗證一下:
type T3 = EventName<"foo" | "bar" | "baz">;
// "fooChanged" | "barChanged" | "bazChanged"
type T4 = Concat<"top" | "bottom", "left" | "right">;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
為什么會生成這樣的類型呢?這是因為對于模板字面量類型來說,當類型占位符的實際類型是聯(lián)合類型(A |B |C)的話,就會被自動展開:
`$ A|B|C ` => ` $ A ` | ` $ B ` | ` $ C `
而對于包含多個類型占位符的情形,比如 Concat 工具類型。多個占位符中的聯(lián)合類型解析為叉積:
`$A|B -$ C|D ` => `$ A -$ C ` | `$ A -$ D ` | `$ B -$ C ` | `$ B -$ D `
了解完上述的運算規(guī)則,你應該就能理解生成的 T3 和 T4 類型了。
在使用模板字面量類型的過程中,我們還可以使用 TypeScript 提供的,用于處理字符串類型的內置工具類型,比如 Uppercase、Lowercase、Capitalize 和 Uncapitalize。具體的使用方式是這樣的:
type GetterName<T extends string> = `get$Capitalize<T> `;
type Cases<T extends string> = `$ Uppercase<T> $ Lowercase<T> $ Capitalize<T> $ Uncapitalize<T> `;
type T5 = GetterName<'foo'>; // "getFoo"
type T6 = Cases<'bar'>; // "BAR bar Bar bar"
其實,模板字面量類型的能力是很強大的,結合 TypeScript 的條件類型和 infer 關鍵字我們還可以實現(xiàn)類型推斷。
type Direction = "left" | "right" | "top" | "bottom";
type InferRoot<T> = T extends `${infer R}${Capitalize<Direction>}` ? R T;
type T7 = InferRoot<"marginRight">; // "margin"
type T8 = InferRoot<"paddingLeft">; // "padding"
在以上代碼中,InferRoot 工具類型除了利用模板字面量類型之外,還使用了 TypeScript 條件類型和 infer。如果你對這兩個知識點,還不了解的話,可以觀看 “用了 TS 條件類型,同事直呼 YYDS?” 和 “學會 TS infer,寫起泛型真香!” 這兩篇文章。
此外,TypeScript 4.1 版本允許我們使用 as 子句對映射類型中的鍵進行重新映射。它的語法如下:
type MappedTypeWithNewKeys<T> =
[K in keyof T as NewKeyType] T K
// ^^^^^^^^^^^^^
// This is the new syntax!
其中 NewKeyType 的類型必須是 string | number | symbol 聯(lián)合類型的子類型。在重新映射的過程中,結合模板字面量類型所提供的能力,我們就可以實現(xiàn)一些有用的工具類型。
比如,我們可以定義一個 Getters 工具類型,用于為對象類型生成對應的 Getter 類型:
type Getters<T> =
[K in keyof T as `get${Capitalize<string & K>}`] () => T K
;
interface Person
name string;
age number;
location string;
type LazyPerson = Getters<Person>;
//
// getName () => string;
// getAge () => number;
// getLocation () => string;
//
在以上代碼中,因為 keyof T 返回的類型可能會包含 symbol 類型,而 Capitalize 工具類型要求處理的類型需要是 string 類型的子類型,所以需要通過交叉運算符進行類型過濾。
除了實現(xiàn)簡單的工具類型之外,我們還可以實現(xiàn)比較復雜的工具類型。比如,用于獲取對象類型中,任意層級屬性的類型。
type PropType<T, Path extends string> = string extends Path
? unknown
Path extends keyof T
? T Path
Path extends `$ infer K .$ infer R `
? K extends keyof T
? PropType<T K , R>
unknown
unknown;
declare function getPropValue<T, P extends string>(
obj T,
path P
) PropType<T, P>;
const obj = { a b c666 d"阿寶哥" ;
let a = getPropValue(obj, "a"); // { b c number, d string
let ab = getPropValue(obj, "a.b"); // {c number, d string
let abd = getPropValue(obj, "a.b.d"); // string
在以上代碼中,PropType 工具類型涉及 TypeScript 中的多個核心知識點。