模板字面量型別基於 字串字面量型別,並具有透過聯合型別擴充套件為多種字串的能力。
它們的語法與 JavaScript 中的模板字面量字串 相同,但用於型別位置。當與具體的字面量型別一起使用時,模板字面量透過拼接內容產生一個新的字串字面量型別。
tsTrytypeWorld = "world";typeGreeting = `hello ${World }`;
當聯合型別用於插值位置時,該型別將是每個聯合成員所能代表的每一個可能的字串字面量的集合。
tsTrytypeEmailLocaleIDs = "welcome_email" | "email_heading";typeFooterLocaleIDs = "footer_title" | "footer_sendoff";typeAllLocaleIDs = `${EmailLocaleIDs |FooterLocaleIDs }_id`;
對於模板字面量中的每個插值位置,聯合型別都會進行交叉相乘(笛卡爾積)。
tsTrytypeAllLocaleIDs = `${EmailLocaleIDs |FooterLocaleIDs }_id`;typeLang = "en" | "ja" | "pt";typeLocaleMessageIDs = `${Lang }_${AllLocaleIDs }`;
我們通常建議開發者在處理大型字串聯合型別時使用提前生成的方法,但這在較小的情況下非常有用。
型別中的字串聯合
模板字面量的強大之處在於根據型別內部的資訊定義新字串。
考慮這樣一種情況:一個函式(makeWatchedObject)向傳入的物件新增一個名為 on() 的新函式。在 JavaScript 中,它的呼叫可能看起來像:makeWatchedObject(baseObject)。我們可以想象基礎物件看起來是這樣的:
tsTryconstpassedObject = {firstName : "Saoirse",lastName : "Ronan",age : 26,};
將要新增到基礎物件中的 on 函式需要兩個引數,一個 eventName(字串 string)和一個 callback(函式 function)。
eventName 應該具有 傳入物件中的屬性名 + "Changed" 的形式;因此,它是從基礎物件的屬性 firstName 派生出的 firstNameChanged。
callback 函式在被呼叫時:
- 應該傳入一個與
傳入物件中的屬性名關聯的型別值;因此,由於firstName被定義為string型別,firstNameChanged事件的回撥函式在呼叫時應該期望接收一個string。類似地,與age關聯的事件應該期望接收一個number引數。 - 應該具有
void返回型別(為了演示簡單起見)。
on() 的簡單函式簽名可能是:on(eventName: string, callback: (newValue: any) => void)。然而,在前面的描述中,我們確定了想要在程式碼中記錄的重要型別約束。模板字面量型別讓我們能夠將這些約束帶入程式碼中。
tsTryconstperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26,});// makeWatchedObject has added `on` to the anonymous Objectperson .on ("firstNameChanged", (newValue ) => {console .log (`firstName was changed to ${newValue }!`);});
請注意,on 監聽的是 "firstNameChanged" 事件,而不僅僅是 "firstName"。如果我們能確保合格的事件名稱集合被限制為“被監視物件中的屬性名加上字尾‘Changed’”構成的聯合型別,那麼我們對 on() 的簡單定義會變得更加健壯。雖然我們在 JavaScript 中可以很輕鬆地進行此類計算,即 Object.keys(passedObject).map(x => `${x}Changed`),但在 型別系統內部 使用模板字面量提供了類似的字串操作方法。
tsTrytypePropEventSource <Type > = {on (eventName : `${string & keyofType }Changed`,callback : (newValue : any) => void): void;};/// Create a "watched object" with an `on` method/// so that you can watch for changes to properties.declare functionmakeWatchedObject <Type >(obj :Type ):Type &PropEventSource <Type >;
有了這個,我們可以構建一個在給定錯誤屬性時會報錯的東西。
tsTryconstperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26});person .on ("firstNameChanged", () => {});// Prevent easy human error (using the key instead of the event name)Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.person .on ("firstName" , () => {});// It's typo-resistantArgument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.person .on ("frstNameChanged" , () => {});
模板字面量型別推斷
請注意,我們並沒有利用原始傳入物件中提供的所有資訊。鑑於 firstName 的變化(即 firstNameChanged 事件),我們應該期望回撥函式接收一個 string 型別的引數。同樣地,age 變化的回撥函式應該接收一個 number 引數。我們天真地使用了 any 來標註 callback 的引數。同樣地,模板字面量型別使我們能夠確保屬性的資料型別與其回撥函式的第一個引數型別一致。
實現這一點的關鍵洞察是:我們可以使用一個帶有泛型的函式,使得:
- 用作第一個引數的字面量被捕獲為字面量型別。
- 該字面量型別可以被驗證為存在於泛型中有效屬性的聯合型別內。
- 可以使用索引訪問(Indexed Access)在泛型的結構中查詢已驗證屬性的型別。
- 然後,可以將此型別資訊應用於確保回撥函式的引數具有相同的型別。
tsTrytypePropEventSource <Type > = {on <Key extends string & keyofType >(eventName : `${Key }Changed`,callback : (newValue :Type [Key ]) => void): void;};declare functionmakeWatchedObject <Type >(obj :Type ):Type &PropEventSource <Type >;constperson =makeWatchedObject ({firstName : "Saoirse",lastName : "Ronan",age : 26});person .on ("firstNameChanged",newName => {console .log (`new name is ${newName .toUpperCase ()}`);});person .on ("ageChanged",newAge => {if (newAge < 0) {console .warn ("warning! negative age");}})
這裡我們將 on 做成了一個泛型方法。
當用戶使用字串 "firstNameChanged" 呼叫時,TypeScript 會嘗試為 Key 推斷正確的型別。為了做到這一點,它會將 Key 與 "Changed" 之前的內容進行匹配,並推斷出字串 "firstName"。一旦 TypeScript 弄清楚了這一點,on 方法就可以獲取原始物件上 firstName 的型別,在這種情況下是 string。類似地,當使用 "ageChanged" 呼叫時,TypeScript 會找到屬性 age 的型別,即 number。
型別推斷可以以不同的方式組合,通常用於解構字串,並以不同的方式重建它們。
內建字串操作型別
為了輔助字串操作,TypeScript 包含了一組可用於字串操作的型別。這些型別為了效能是編譯器內建的,不能在 TypeScript 附帶的 .d.ts 檔案中找到。
Uppercase<StringType>
將字串中的每個字元轉換為大寫版本。
示例
tsTrytypeGreeting = "Hello, world"typeShoutyGreeting =Uppercase <Greeting >typeASCIICacheKey <Str extends string> = `ID-${Uppercase <Str >}`typeMainID =ASCIICacheKey <"my_app">
Lowercase<StringType>
將字串中的每個字元轉換為小寫版本。
示例
tsTrytypeGreeting = "Hello, world"typeQuietGreeting =Lowercase <Greeting >typeASCIICacheKey <Str extends string> = `id-${Lowercase <Str >}`typeMainID =ASCIICacheKey <"MY_APP">
Capitalize<StringType>
將字串的第一個字元轉換為大寫版本。
示例
tsTrytypeLowercaseGreeting = "hello, world";typeGreeting =Capitalize <LowercaseGreeting >;
Uncapitalize<StringType>
將字串的第一個字元轉換為小寫版本。
示例
tsTrytypeUppercaseGreeting = "HELLO WORLD";typeUncomfortableGreeting =Uncapitalize <UppercaseGreeting >;
關於內建字串操作型別的技術細節
截至 TypeScript 4.1,這些內建函式的程式碼直接使用 JavaScript 執行時字串函式進行操作,且不感知本地化。
function applyStringMapping(symbol: Symbol, str: string) {
switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
}
return str;
}