首頁  >  文章  >  web前端  >  了解TypeScript資料類型中的範本字面量

了解TypeScript資料類型中的範本字面量

青灯夜游
青灯夜游轉載
2021-12-17 10:40:452133瀏覽

TypeScript團隊發布了TypeScript 4.1,其中包括強大的模板字面量類型、映射類型的鍵重映射以及遞歸條件類型。以下這篇文章就來帶大家了解一下TypeScript中的模板字面量類型,希望對大家有幫助!

了解TypeScript資料類型中的範本字面量

範本字面類型(Template Literal Types)

範本字面類型以字串字面量類型為基礎,可以透過聯合類型擴展成多個字串。

它們跟 JavaScript 的模板字串是相同的語法,但是只能用在型別操作中。當使用模板字面量類型時,它會替換模板中的變量,返回一個新的字串字面量:

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

當模板中的變量是一個聯合類型時,每一個可能的字串字面量都會被表示:

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
 
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

如果模板字面量裡的多個變數都是聯合類型,結果會交叉相乘,例如下面的例子就有2 2 3 一共12 種結果:

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
 
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"

如果真的是非常長的字串聯合類型,推薦提前生成,這種還是適用於短一些的情況。

類型中的字串聯合類型(String Unions in Types)

模板字面量最有用的地方在於你可以基於一個類型內部的信息,定義一個新的字串,讓我們舉個例子:

有這樣一個函數makeWatchedObject, 它會為傳入的物件增加了一個on  方法。在JavaScript 中,它的呼叫看起來是這樣:makeWatchedObject(baseObject),我們假設這個傳入物件為:

const passedObject = {
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
};

這個on 方法會被添加到這個傳入物件上,該方法接受兩個參數,eventNamestring 類型) 和callBackfunction 類型):

// 伪代码
const result = makeWatchedObject(baseObject);
result.on(eventName, callBack);

我們希望eventName 是這種形式:attributeInThePassedObject "Changed" ,舉個例子,passedObject 有一個屬性firstName ,對應產生的eventNamefirstNameChanged,同理,lastName 對應的是lastNameChangedage 對應的是ageChanged

當這個 callBack 函數被呼叫的時候:

  • 應該被傳入與 attributeInThePassedObject 相同類型的值。例如passedObject 中, firstName 的值的型別為string , 對應firstNameChanged 事件的回呼函數,則接受傳入一個string  類型的值。 age 的值的型別為 number,對應 ageChanged 事件的回呼函數,則接受傳入一個 number 類型的值。
  • 傳回值類型為 void 類型。

on() 方法的簽章最一開始是這樣的:on(eventName: string, callBack: (newValue: any) => void)。使用這樣的簽名,我們是不能實現上面所說的這些約束的,這個時候就可以使用模板字面量:

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26,
});
 
// makeWatchedObject has added `on` to the anonymous Object
person.on("firstNameChanged", (newValue) => {
  console.log(`firstName was changed to ${newValue}!`);
});

注意這個例子裡,on 方法添加的事件名為"firstNameChanged",而不僅僅是"firstName",而回呼函數傳入的值newValue ,我們希望約束為string 類型。我們先實現第一點。

在這個例子裡,我們希望傳入的事件名稱的類型,是物件屬性名稱的聯合,只是每個聯合成員都還在最後拼接一個Changed 字符,在JavaScript中,我們可以做這樣一個計算:

Object.keys(passedObject).map(x => ${x}Changed)

模板字面量提供了一個相似的字串操作:

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
 
/// Create a "watched object" with an &#39;on&#39; method
/// so that you can watch for changes to properties.

declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

注意,我們在這裡例子中,模板字面量裡我們寫的是string & keyof Type,我們可不可以只寫成keyof Type 呢?如果我們這樣寫,會報錯:

type PropEventSource<Type> = {
    on(eventName: `${keyof Type}Changed`, callback: (newValue: any) => void): void;
};

// Type &#39;keyof Type&#39; is not assignable to type &#39;string | number | bigint | boolean | null | undefined&#39;.
// Type &#39;string | number | symbol&#39; is not assignable to type &#39;string | number | bigint | boolean | null | undefined&#39;.
// ...

從報錯訊息中,我們也可以看出報錯原因,在《TypeScript 系列之Keyof 運算子》裡,我們知道keyof 運算子會回傳string | number | symbol 類型,但是模板字面量的變數要求的型別卻是string | number | bigint | boolean | null | undefined,比較一下,多了一個symbol 類型,所以其實我們也可以這樣寫:

type PropEventSource<Type> = {
    on(eventName: `${Exclude<keyof Type, symbol>}Changed`, callback: (newValue: any) => void): void;
};

再或這樣寫:

type PropEventSource<Type> = {
     on(eventName: `${Extract<keyof Type, string>}Changed`, callback: (newValue: any) => void): void;
};

使用這種方式,當我們使用錯誤的事件名稱時,TypeScript 會給出錯誤:

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", () => {});
 
// Prevent easy human error (using the key instead of the event name)
person.on("firstName", () => {});
// Argument of type &#39;"firstName"&#39; is not assignable to parameter of type &#39;"firstNameChanged" | "lastNameChanged" | "ageChanged"&#39;.
 
// It&#39;s typo-resistant
person.on("frstNameChanged", () => {});
// Argument of type &#39;"frstNameChanged"&#39; is not assignable to parameter of type &#39;"firstNameChanged" | "lastNameChanged" | "ageChanged"&#39;.

模板字面量的推論(Inference with Template Literals)

現在我們來實作第二點,回呼函數傳入的值的型別與對應的屬性值的類型相同。我們現在只是簡單的對 callBack 的參數使用 any 類型。實現這個約束的關鍵在於藉助泛型函數:

  • 捕获泛型函数第一个参数的字面量,生成一个字面量类型

  • 该字面量类型可以被对象属性构成的联合约束

  • 对象属性的类型可以通过索引访问获取

  • 应用此类型,确保回调函数的参数类型与对象属性的类型是同一个类型

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
};
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});
 
person.on("firstNameChanged", newName => {                             
                                                          // (parameter) newName: string
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
person.on("ageChanged", newAge => {
                        // (parameter) newAge: number
    if (newAge < 0) {
        console.warn("warning! negative age");
    }
})

这里我们把 on 改成了一个泛型函数。

当一个用户调用的时候传入 "firstNameChanged",TypeScript 会尝试着推断 Key 正确的类型。它会匹配 key"Changed" 前的字符串 ,然后推断出字符串 "firstName" ,然后再获取原始对象的 firstName 属性的类型,在这个例子中,就是 string 类型。

内置字符操作类型(Intrinsic String Manipulation Types)

TypeScript 的一些类型可以用于字符操作,这些类型处于性能的考虑被内置在编译器中,你不能在 .d.ts 文件里找到它们。

Uppercasea24091e5aa4cbf3564292ba7dbe442c1

把每个字符转为大写形式:

type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>        
// type ShoutyGreeting = "HELLO, WORLD"
 
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
// type MainID = "ID-MY_APP"

Lowercasea24091e5aa4cbf3564292ba7dbe442c1

把每个字符转为小写形式:

type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>       
// type QuietGreeting = "hello, world"
 
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">    
// type MainID = "id-my_app"

Capitalizea24091e5aa4cbf3564292ba7dbe442c1

把字符串的第一个字符转为大写形式:

type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// type Greeting = "Hello, world"

Uncapitalizea24091e5aa4cbf3564292ba7dbe442c1

把字符串的第一个字符转换为小写形式:

type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;           
// type UncomfortableGreeting = "hELLO WORLD"

字符操作类型的技术细节

从 TypeScript 4.1 起,这些内置函数会直接使用 JavaScript 字符串运行时函数,而不是本地化识别 (locale aware)。

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;
}

【相关推荐:javascript学习教程

以上是了解TypeScript資料類型中的範本字面量的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除