用了模板字面量类型,同事直呼太强了!
你了解过模板字面量类型么?用模你想知道如何利用模板字面量类型,来减少 TypeScript 项目中的板字重复代码么?如果想的话,阅读完本文内容之后,面量也许你就懂了。类型
假设我们想要定义一种类型来描述 CSS padding 规则,同事太强如果你了解 TypeScript 类型别名和联合类型的直呼话,能很容易定义出 CssPadding 类型。用模
type CssPadding =
| "padding-left"
| "padding-right"
| "padding-top"
| "padding-bottom";但如果我们想要继续定义一种新的板字类型来描述 CSS margin 规则,你是面量不是立马想到与定义 CssPadding 类型一样的方式。
type MarginPadding =
| "margin-left"
| "margin-right"
| "margin-top"
| "margin-bottom";对于以上定义的类型两种类型来说,虽然它们都能满足我们的同事太强需求。但在定义这两种类型的直呼过程中,仍然存在一些重复的用模代码。
那么如何解决这个问题呢?板字这时我们可以使用 TypeScript 4.1 版本引入了新的模板字面量类型,具体的面量使用方式如下:
type Direction = "left" | "right" | "top" | "bottom";
type CssPadding = `padding-${ Direction}`;
type MarginPadding = `margin-${ Direction}`;看完以上代码,是不是觉得简洁很多。与 JavaScript 中的模板字符串类似,模板字面量类型被括在反引号中,网站模板同时可以包含 ${ T} 形式的占位符,其中类型变量 T 的类型可以是 string、number、boolean 或 bigint 类型。
模板字面量类型不仅为我们提供了连接字符串字面量的能力,而且还可以把非字符串基本类型的字面量转换为对应的字符串字面量类型。下面我们来举一些具体的例子:
type EventName
type Concat
type ToString
type T0 = EventName<"foo">; // fooChanged
type T1 = Concat<"Hello", "World">; // Hello-World
type T2 = ToString<"阿宝哥" | 666 | true | -1234n>; // "阿宝哥" | "true" | "666" | "-1234"对于上述的例子来说,其实并不复杂。但现在问题来了,如果传入 EventName 或 Concat 工具类型的实际类型是联合类型的话,那么结果又会是怎样呢?接下来,我们来验证一下:
type T3 = EventName<"foo" | "bar" | "baz">;
// "fooChanged" | "barChanged" | "bazChanged"
type T4 = Concat<"top" | "bottom", "left" | "right">;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"为什么会生成这样的类型呢?这是因为对于模板字面量类型来说,当类型占位符的实际类型是联合类型(A |B |C)的话,就会被自动展开:
`[${ A|B|C}]` => `[${ A}]` | `[${ B}]` | `[${ C}]`而对于包含多个类型占位符的情形,比如 Concat 工具类型。云服务器提供商多个占位符中的联合类型解析为叉积:
`${ A|B}-${ C|D}` => `${ A}-${ C}` | `${ A}-${ D}` | `${ B}-${ C}` | `${ B}-${ D}`了解完上述的运算规则,你应该就能理解生成的 T3 和 T4 类型了。
在使用模板字面量类型的过程中,我们还可以使用 TypeScript 提供的,用于处理字符串类型的内置工具类型,比如 Uppercase、Lowercase、Capitalize 和 Uncapitalize。具体的使用方式是这样的:
type GetterName
type Cases
type T5 = GetterName
其实,模板字面量类型的能力是很强大的,结合 TypeScript 的条件类型和 infer 关键字我们还可以实现类型推断。
type Direction = "left" | "right" | "top" | "bottom";
type InferRoot
type T7 = InferRoot<"marginRight">; // "margin"
type T8 = InferRoot<"paddingLeft">; // "padding"在以上代码中,InferRoot 工具类型除了利用模板字面量类型之外,还使用了 TypeScript 条件类型和 infer。如果你对这两个知识点,还不了解的话,可以观看 “用了 TS 条件类型,同事直呼 YYDS” 和 “学会 TS infer,写起泛型真香!源码库” 这两篇文章。
此外,TypeScript 4.1 版本允许我们使用 as 子句对映射类型中的键进行重新映射。它的语法如下:
type MappedTypeWithNewKeys
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}其中 NewKeyType 的类型必须是 string | number | symbol 联合类型的子类型。在重新映射的过程中,结合模板字面量类型所提供的能力,我们就可以实现一些有用的工具类型。
比如,我们可以定义一个 Getters 工具类型,用于为对象类型生成对应的 Getter 类型:
type Getters
[K in keyof T as `get${ Capitalize
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }在以上代码中,因为 keyof T 返回的类型可能会包含 symbol 类型,而 Capitalize 工具类型要求处理的类型需要是 string 类型的子类型,所以需要通过交叉运算符进行类型过滤。
除了实现简单的工具类型之外,我们还可以实现比较复杂的工具类型。比如,用于获取对象类型中,任意层级属性的类型。
type PropType
? unknown
: Path extends keyof T
? T[Path]
: Path extends `${ infer K}.${ infer R}`
? K extends keyof T
? PropType
: unknown
: unknown;
declare function getPropValue
obj: T,
path: P
): PropType
const obj = { a: { b: { c: 666, d: "阿宝哥" } } };
let a = getPropValue(obj, "a"); // { b: { c: number, d: string } }
let ab = getPropValue(obj, "a.b"); // { c: number, d: string }
let abd = getPropValue(obj, "a.b.d"); // string在以上代码中,PropType 工具类型涉及 TypeScript 中的多个核心知识点。