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"”

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

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

type TextAlign = "left" | "center" | "right" | "justify" | "start" | "end" | "revert" | "unset";

function configure (textAlign: TextAlign) {
  // ...
}

configure("auto");  // 类型“"auto"”的参数不能赋给类型“TextAlign”的参数

configure("left");  // 没毛病

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

模版字面量类型

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

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'

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

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"

如果模板字面量里的多个变量都是联合类型,结果会交叉相乘,如:

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"

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;

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

Uppercase

把每个字符转为大写形式

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"

Lowercase

把每个字符转为小写形式

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"

Capitalize

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

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

Uncapitalize

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

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

字符操作类型的技术细节
从 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. 基于一个类型内部的信息,定义一个新的字符串
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");
  }
});

此时person类型为

const person: {
    firstName: string;
    lastName: string;
    age: number;
} & PropEventSource<{
    firstName: string;
    lastName: string;
    age: number;
}>

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

  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"]

这个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. 配合 … 拓展运算符和 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 = ""

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. 实现使用“点路径”访问属性
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

参考资料

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

你可能感兴趣的:(TypeScript基础,TypeScript,模版字面量类型)