文中内容都是参考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种结果。
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 内部 用到的。
把每个字符转为大写形式
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"
把每个字符转为小写形式
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"
把字符串的第一个字符转为大写形式
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// type Greeting = "Hello, world"
把字符串的第一个字符转换为小写形式
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;
}
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;
}>
回调函数传入的值的类型与对应的属性值的类型相同, 实现这个约束的关键在于借助泛型函数:
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
Join
和 Split
功能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[]
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