• TypeScript基础之模版字面量类型


    前言

    文中内容都是参考https://www.typescriptlang.org/docs/handbook/2/template-literal-types.htm
    TypeScript 之模板字面量类型 — mqyqingfeng 以及
    https://github.com/microsoft/TypeScript/pull/40336内容。

    字符串字面量类型

    const stringLiteral = "https"; 
    // const stringLiteral: "https"
    
    let str: "hello" = "hello";
    str = "string";
    // 不能将类型“"string"”分配给类型“"hello"”
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    const 声明的常量如果赋值为普通类型,其变量值不能进行修改, 所以推断为字面量类型是非常合适的。 它保留了赋值的准确类型信息。
    变量str是使用一个字符串字面量类型作为变量类型。

    实际上, 定义单个的字面量类型并没有太大用处,它真正的应用场景是可以把多个字面量类型组合成一个联合类型,用来描述拥有明确成员的实用的集合。
    如:

    type TextAlign = "left" | "center" | "right" | "justify" | "start" | "end" | "revert" | "unset";
    
    function configure (textAlign: TextAlign) {
      // ...
    }
    
    configure("auto");  // 类型“"auto"”的参数不能赋给类型“TextAlign”的参数
    
    configure("left");  // 没毛病
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    关于字符串字面量类型就简单介绍到这。

    模版字面量类型

    模板字面量类型以字符串字面量类型为基础,可以通过联合类型扩展成多个字符串。

    JavaScript 的模板字符串是相同的语法,但是只能用在类型操作中。当使用模板字面量类型时,它会替换模板中的变量,返回一个新的字符串字面量:

    type World = "world";
     
    type Greeting = `hello ${World}`;
    // type Greeting = "hello world"
    
    
    type EventName<T extends string> = `${T}Changed`;
    type T0 = EventName<'foo'>;  // 'fooChanged'
    type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
    
    
    type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
    type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    当模板中的变量是一个联合类型时,每一个可能的字符串字面量都会被表示:

    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"
    
    type T = `${'top' | 'bottom'}-${'left' | 'right'}`;  
    // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
    
    type ToString<T extends string | number | boolean | bigint> = `${T}`;
    type T1 = ToString<'abc' | 42 | true | -1234n>;  
    // type T1 = "abc" | "true" | "42" | "-1234"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    LocaleMessageIDs类型就有2 * 2 * 3 = 12种结果。

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

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

    https://github.com/microsoft/TypeScript/blob/main/src/lib/es5.d.ts中定义:

    /**
     * Convert string literal type to uppercase
     */
    type Uppercase<S extends string> = intrinsic;
    
    /**
     * Convert string literal type to lowercase
     */
    type Lowercase<S extends string> = intrinsic;
    
    /**
     * Convert first character of string literal type to uppercase
     */
    type Capitalize<S extends string> = intrinsic;
    
    /**
     * Convert first character of string literal type to lowercase
     */
    type Uncapitalize<S extends string> = intrinsic;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    intrinsic 是 typescript 引入的一个关键字,就如它的含义一样,是TS 内部 用到的。

    Uppercase<StringType>

    把每个字符转为大写形式

    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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Lowercase<StringType>

    把每个字符转为小写形式

    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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Capitalize<StringType>

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

    type LowercaseGreeting = "hello, world";
    type Greeting = Capitalize<LowercaseGreeting>;
    // type Greeting = "Hello, world"
    
    • 1
    • 2
    • 3

    Uncapitalize<StringType>

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

    type UppercaseGreeting = "HELLO WORLD";
    type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;           
    // type UncomfortableGreeting = "hELLO WORLD"
    
    • 1
    • 2
    • 3

    字符操作类型的技术细节
    从 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    高级使用

    1. 基于一个类型内部的信息,定义一个新的字符串
    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,
    });
    
    // (parameter) newName: string
    person.on("firstNameChanged", (newName) => {
      console.log(`new name is ${newName.toUpperCase()}`);
    });
    
    // (parameter) newAge: number
    person.on("ageChanged", (newAge) => {
      if (newAge < 0) {
        console.warn("warning! negative age");
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    此时person类型为

    const person: {
        firstName: string;
        lastName: string;
        age: number;
    } & PropEventSource<{
        firstName: string;
        lastName: string;
        age: number;
    }>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    回调函数传入的值的类型与对应的属性值的类型相同, 实现这个约束的关键在于借助泛型函数:

    1. 捕获泛型函数第一个参数的字面量,生成一个字面量类型
    2. 该字面量类型可以被对象属性构成的联合约束
    3. 对象属性的类型可以通过索引访问获取
    4. 应用此类型,确保回调函数的参数类型与对象属性的类型是同一个类型

    1. 配合infer 使用
      模板字符串可以通过 infer 关键字,实现类似于正则匹配提取的功能:
    type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown;
    
    type T0 = MatchPair<'[1,2]'>;  // type T0 = ["1", "2"]
    type T1 = MatchPair<'[foo,bar]'>;  // type T1 = ["foo", "bar"]
    type T2 = MatchPair<' [1,2]'>;  // type T2 = unknown
    type T3 = MatchPair<'[123]'>;  // type T3 = unknown
    type T4 = MatchPair<'[1,2,3,4]'>;  // type T4 = ["1", "2,3,4"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个infer其实就相当于占位,也就是用infer X去给一个不知道的类型占位。
    通过 , 分割左右两边,再在左右两边分别用一个 infer 泛型接受推断值 [infer A, infer B],就可以轻松的重新组合 , 两边的字符串。

    type FirstTwoAndRest<S extends string> = S extends `${infer A}${infer B}${infer R}` ? [`${A}${B}`, R] : unknown;
    
    type T0 = FirstTwoAndRest<'abcde'>;  // type T0 = ["ab", "cde"]
    type T1 = FirstTwoAndRest<'ab'>;  // type T1 = ["ab", ""]
    type T2 = FirstTwoAndRest<'a'>;  // type T2 = unknown
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1. 配合 … 拓展运算符和 infer递归,实现 JoinSplit 功能
    type Join<T extends unknown[], D extends string> =
        T extends [] ? '' :
        T extends [string | number | boolean | bigint] ? `${T[0]}` :
        T extends [string | number | boolean | bigint, ...infer U] ? `${T[0]}${D}${Join<U, D>}` :
        string;
    
    type T0 = Join<[1, 2, 3, 4], '.'>;  // type T0 = "1.2.3.4"
    
    type T1 = Join<['foo', 'bar', 'baz'], '-'>;  // type T1 = "foo-bar-baz"
    
    type T2 = Join<[], '.'>;  // type T2 = ""
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Split

    type Split<S extends string, D extends string> =
        string extends S ? string[] :
        S extends '' ? [] :
        S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
        [S];
    
    type T0 = Split<'foo', '.'>;  // type T0 = ["foo"]
    type T1 = Split<'foo.bar.baz', '.'>;  // type T1 = ["foo", "bar", "baz"]
    type T2 = Split<'foo.bar', ''>;  // type T2 = ["f", "o", "o", ".", "b", "a", "r"]
    type T3 = Split<any, '.'>;  // type T3 = string[]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    1. 实现使用“点路径”访问属性
    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>;
    declare const s: string;
    
    const obj = { a: { b: {c: 42, d: 'hello' }}};
    getPropValue(obj, 'a');  // { b: {c: number, d: string } }
    getPropValue(obj, 'a.b');  // {c: number, d: string }
    getPropValue(obj, 'a.b.d');  // string
    getPropValue(obj, 'a.b.x');  // unknown
    getPropValue(obj, s);  // unknown
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    参考资料

    https://www.typescriptlang.org/docs/handbook/2/template-literal-types.htm
    TypeScript 之模板字面量类型 — mqyqingfeng
    https://github.com/microsoft/TypeScript/pull/40336

  • 相关阅读:
    配置与管理Samba服务器实例
    【反射】Class类
    如何建立编程思想和提高编程思想
    HTML 13 HTML a 标签
    两数相加 js
    数据量太大了,数据分批处理
    【Linux】 rm命令使用
    kotlin 泛型
    怎么样显卡叠加,什么是NVIDIA 显卡 非公、公版、涡轮卡
    掌握Flink键控状态(Keyed State):深入指南与实践
  • 原文地址:https://blog.csdn.net/zxl1990_ok/article/details/125417699