写作背景:󠀰

  1. 承接上一篇[扒官方文档学Ts类型编程](https://juejin.cn/post/7084128507109834782#comment)来继续扒完类型编程的后两个章节Mapped Types和Template Literal Types,同样准备了演练场的代码可以同步调试观察输出的类型来学习。

TypeScript类型操作:

TypeScript类型系统的强大之处主要体现在它允许我们通过类型来表达类型,也就是说我们可以通过现有的类型经过一系列的操作得到另一个类型(从类型创建类型),我们将通过下面表格所列举的顺序来讲解如何表达一个新的类型:

序号 Types 类型 描述
1 Generics-Types 泛型类型 带参数的类型
2 Keyof Type Operator Keyof 类型运算符 使用keyof运算符创建新类型
3 Typeof Type Operator Typeof 类型运算符 使用typeof运算符创建新类型
4 Indexed Access Types 索引访问类型 使用Type[‘a’]语法访问类型的子集
5 Conditional Types 条件类型 行为类似于类型系统中的 if 语句的类型
6 Mapped Types 映射类型 通过映射现有类型中的每个属性来创建类型
7 Template Literal Types 模板字符串类型 通过模板字符串更改属性的映射类型

Mapped Types:

通过使用映射类型可以再不重新定义的前提下创建另一种新的类型,映射类型也是一种通用类型,接下来我们通过一系列的示例来感受一下映射类型的使用。

正式开始前需要明确以下4点:

  1. 使用映射类型的最基础是通过索引类型访问来实现的;
  2. 使用映射类型前应该有一个类型;
  3. 使用keyof关键字来得到输入类型中key组成的联合类型;
  4. 使用in关键字可以遍历联合类型。

入门案例:HelloWorld

了解一个最简单的映射类型工具的使用。

下面这块代码是我们待输入的类型:

  1. type FeatureFlags = {
  2. darkMode: () => void;
  3. newUserProfile: () => void;
  4. };

类型工具说明:

借助下面的通用映射类型工具,将输入类型Type的key来作为新对象类型的key,value的类型统一为boolean类型进行约束。

  1. type OptionsFlags<Type> = {
  2. [Property in keyof Type]: boolean;
  3. };

我们使用一下这个类型工具:

  1. type FeatureOptions = OptionsFlags<FeatureFlags>;
  2. // ^?

去演练场验证结果

案例-在映射类型中使用修饰符:

在映射类型中同样支持在JavaScript中对对象的修饰属性,readonly,属性可选。在映射类型中还可以对已经存在的这些修饰符进行删除。

剔除输入类型中的readonly修饰

下面这块代码是我们待输入的类型:
  1. type LockedAccount = {
  2. readonly id: string;
  3. readonly name: string;
  4. };

类型工具说明:

借助下面的通用的映射类型工具,将输入类型中的readonly删除掉,得到一个所有属性均非只读的对象类型;

  1. type CreateMutable<Type> = {
  2. -readonly [Property in keyof Type]: Type[Property];
  3. };

我们使用一下这个类型工具:
  1. type UnlockedAccount = CreateMutable<LockedAccount>;
  2. // ^?

去演练场验证答案

剔除输入类型中的可选修饰:

下面这块代码是我们待输入的类型
  1. type MaybeUser = {
  2. id: string;
  3. name?: string;
  4. age?: number;
  5. };

类型工具说明:

借助下面的通用的映射类型工具,将输入类型中的可选修饰符删除掉,得到一个所有属性均必填的对象类型;

  1. type Concrete<Type> = {
  2. [Property in keyof Type]-?: Type[Property];
  3. };

我们使用一下这个类型工具:
  1. type User = Concrete<MaybeUser>;
  2. // ^?

去演练场验证答案

案例-搭配模板字符类型使用

模板字符类型可以方便我们链接/扩展为更多的字符串类型,使用类同我们在JavaScript中模板字符串的使用,但模板类型作用在类型位置。
模块字符类型我们会在下面单独讲,这个案例使用到的Capitalize会将传入的Property转为首字母大写。

下面这块代码是我们待输入的类型

  1. interface Person {
  2. name: string;
  3. age: number;
  4. location: string;
  5. }

类型工具说明:

借助下面的通用的映射类型工具,我们可以为输入的类型Type增加对应的已get为前缀的函数。我们通常在定义完对象属性后会增加对应属性获取的函数而不是直接对外暴露这个属性。

  1. type Getters<Type> = {
  2. [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
  3. };

我们使用一下这个类型工具:

  1. type LazyPerson = Getters<Person>;
  2. // ^?

去演练场验证答案

案例-映射类型+内置的类型工具

使用内置的类型工具Exclude来配合映射类型剔除掉输入类型的指定属性后创建一个新的类型。

下面这块代码是我们待输入的类型

  1. interface Circle {
  2. kind: "circle";
  3. radius: number;
  4. }

类型工具说明:

借助下面的通用的映射类型工具,在使用索引类型方式key时使用as关键字来对Property进行进一步的处理,使用Exclude剔除掉所包含的kind,从而得到一个新的类型。

  1. type RemoveKindField<Type> = {
  2. [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
  3. };

我们使用一下这个类型工具:

  1. type KindlessCircle = RemoveKindField<Circle>;
  2. // ^?

去演练场验证答案

案例-映射联合类型

下面这块代码是我们待输入的类型

  1. type SquareEvent = { kind: "square", x: number, y: number };
  2. type CircleEvent = { kind: "circle", radius: number };

类型工具说明:

借助下面的通用的映射类型工具,在输入类型做了约束,必须要包含king且类型为string的属性,在遍历Events的时候使用as取E中名为kind的value作为新类型的key。

  1. type EventConfig<Events extends { kind: string }> = {
  2. [E in Events as E["kind"]]: (event: E) => void;
  3. }

我们使用一下这个类型工具:

  1. type Config = EventConfig<SquareEvent | CircleEvent>
  2. // ^?

去演练场验证答案

Template Literal Types:

模板字符类型的语法同JavaScript中模板字符串,但使用的位置不同,模板字符类型应用在类型位置。通过使用模板类型来扩展/链接内容创建新的字符类型。

入门案例:HelloWorld

下面使我们的入门案例,通过模板插值将类型World插入到了类型Greeting中,最终创建的类型为“hello world”,注意这里面的“hello world”是类型而不是值。

  1. type World = "world";
  2. type Greeting = `hello ${World}`;
  3. // ^?

去演练场验证答案

案例-插值位置使用联合类型:

下面的案例可以得到一个结果,当插值位置是用联合类型是,结果将是由每个联合成员便是的每个可能的字符串组成的集合。

  1. type EmailLocaleIDs = "welcome_email" | "email_heading";
  2. type FooterLocaleIDs = "footer_title" | "footer_sendoff";
  3. type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
  4. // ^?

去演练场验证答案

案例-多个插值位置使用联合类型:

下面的案例可以得到一个结果,当每个插值位置均使用联合类型时,结果的数量将是每个联合元素个数相乘的积。

  1. type EmailLocaleIDs = "welcome_email" | "email_heading";
  2. type FooterLocaleIDs = "footer_title" | "footer_sendoff";
  3. type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
  4. type Lang = "en" | "ja" | "pt";
  5. type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
  6. // ^?

去演练场验证答案

案例-在类型中使用字符串联合

案例介绍:

  1. 我们通常会在做一系列操作的函数前面定义一个明显的前缀;
  2. 在这个案例中实现监听对象属性的变化;
  3. 需要有一个on函数来接收,监听事件的名称我们做特定的约束格式为:key+Changed;callback是一个没有返回值的函数。
  1. type PropEventSource<Type> = {
  2. on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
  3. };
  4. /// Create a "watched object" with an 'on' method
  5. /// so that you can watch for changes to properties.
  6. declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
  1. const person = makeWatchedObject({
  2. firstName: "Saoirse",
  3. lastName: "Ronan",
  4. age: 26
  5. });
  6. person.on("firstNameChanged", () => {});
  7. // Prevent easy human error (using the key instead of the event name)
  8. person.on("firstName", () => {});
  9. // It's typo-resistant
  10. person.on("frstNameChanged", () => {});

去演练场验证答案

案例-模板字符类型推导

这个案例是上一个案例的升级版本,增加了对返回类型的推导,使得这个类型工具将更加的完善。

  1. type PropEventSource<Type> = {
  2. on<Key extends string & keyof Type>
  3. (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
  4. };
  5. declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
  1. const person = makeWatchedObject({
  2. firstName: "Saoirse",
  3. lastName: "Ronan",
  4. age: 26
  5. });
  6. person.on("firstNameChanged", newName => {
  7. console.log(`new name is ${newName.toUpperCase()}`);
  8. });
  9. person.on("ageChanged", newAge => {
  10. if (newAge < 0) {
  11. console.warn("warning! negative age");
  12. }
  13. })

去演练场验证答案

整理内置的模板字符操作类型:

将每个字符均转为大写:

  1. type Greeting = "Hello, world"
  2. type ShoutyGreeting = Uppercase<Greeting>
  3. // ^?
  4. type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
  5. type MainID = ASCIICacheKey<"my_app">
  6. // ^?

去演练场验证答案

将每个字符均转为小写:

  1. type Greeting = "Hello, world"
  2. type QuietGreeting = Lowercase<Greeting>
  3. // ^?
  4. type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
  5. type MainID = ASCIICacheKey<"MY_APP">
  6. // ^?

去演练场验证答案

将第一个字符转为大写

  1. type LowercaseGreeting = "hello, world";
  2. type Greeting = Capitalize<LowercaseGreeting>;
  3. // ^?

去演练场验证答案

将第一个字符转为小写:

  1. type UppercaseGreeting = "HELLO WORLD";
  2. type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
  3. // ^?

去演练场验证答案

写到最后:

至此TypeScript类型编程的7大块内容就已经过了一遍了,模板字符类型的案例还需要多熟悉熟悉。在官网还有一些提供的内容类型工具可以直接供我们在实际开发中使用,这里给出Utility Types的地址方便大家查询。类型编程和我们以往的编程一样,同样在乎基础的学习和大量的练习。上次推荐的开源类型挑战项目type-challenges你有练习打卡吗?