写作背景:
承接上一篇扒官方文档学Ts类型编程来继续扒完类型编程的后两个章节 Mapped Types 和 Template Literal Types,同样准备了演练场的代码可以同步调试观察输出的类型来学习。
TypeScript 类型操作:
TypeScript 类型系统的强大之处主要体现在它允许我们通过类型来表达类型,也就是说我们可以通过现有的类型经过一系列的操作得到另一个类型(从类型创建类型),我们将通过下面表格所列举的顺序来讲解如何表达一个新的类型:
Mapped Types:
通过使用映射类型可以再不重新定义的前提下创建另一种新的类型,映射类型也是一种通用类型,接下来我们通过一系列的示例来感受一下映射类型的使用。
正式开始前需要明确以下 4 点:
使用映射类型的最基础是通过索引类型访问来实现的;
使用映射类型前应该有一个类型;
使用 keyof 关键字来得到输入类型中 key 组成的联合类型;
使用 in 关键字可以遍历联合类型。
入门案例:HelloWorld
了解一个最简单的映射类型工具的使用。
下面这块代码是我们待输入的类型:
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
复制代码
类型工具说明:
借助下面的通用映射类型工具,将输入类型 Type 的 key 来作为新对象类型的 key,value 的类型统一为 boolean 类型进行约束。
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
复制代码
我们使用一下这个类型工具:
type FeatureOptions = OptionsFlags<FeatureFlags>;
// ^?
复制代码
去演练场验证结果
案例-在映射类型中使用修饰符:
在映射类型中同样支持在 JavaScript 中对对象的修饰属性,readonly,属性可选。在映射类型中还可以对已经存在的这些修饰符进行删除。
剔除输入类型中的 readonly 修饰
下面这块代码是我们待输入的类型:
type LockedAccount = {
readonly id: string;
readonly name: string;
};
复制代码
类型工具说明:
借助下面的通用的映射类型工具,将输入类型中的 readonly 删除掉,得到一个所有属性均非只读的对象类型;
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
复制代码
我们使用一下这个类型工具:
type UnlockedAccount = CreateMutable<LockedAccount>;
// ^?
复制代码
去演练场验证答案
剔除输入类型中的可选修饰:
下面这块代码是我们待输入的类型
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
复制代码
类型工具说明:
借助下面的通用的映射类型工具,将输入类型中的可选修饰符删除掉,得到一个所有属性均必填的对象类型;
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
复制代码
我们使用一下这个类型工具:
type User = Concrete<MaybeUser>;
// ^?
复制代码
去演练场验证答案
案例-搭配模板字符类型使用
模板字符类型可以方便我们链接/扩展为更多的字符串类型,使用类同我们在 JavaScript 中模板字符串的使用,但模板类型作用在类型位置。模块字符类型我们会在下面单独讲,这个案例使用到的 Capitalize 会将传入的 Property 转为首字母大写。
下面这块代码是我们待输入的类型
interface Person {
name: string;
age: number;
location: string;
}
复制代码
类型工具说明:
借助下面的通用的映射类型工具,我们可以为输入的类型 Type 增加对应的已 get 为前缀的函数。我们通常在定义完对象属性后会增加对应属性获取的函数而不是直接对外暴露这个属性。
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
复制代码
我们使用一下这个类型工具:
type LazyPerson = Getters<Person>;
// ^?
复制代码
去演练场验证答案
案例-映射类型+内置的类型工具
使用内置的类型工具 Exclude 来配合映射类型剔除掉输入类型的指定属性后创建一个新的类型。
下面这块代码是我们待输入的类型
interface Circle {
kind: "circle";
radius: number;
}
复制代码
类型工具说明:
借助下面的通用的映射类型工具,在使用索引类型方式 key 时使用 as 关键字来对 Property 进行进一步的处理,使用 Exclude 剔除掉所包含的 kind,从而得到一个新的类型。
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
复制代码
我们使用一下这个类型工具:
type KindlessCircle = RemoveKindField<Circle>;
// ^?
复制代码
去演练场验证答案
案例-映射联合类型
下面这块代码是我们待输入的类型
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
复制代码
类型工具说明:
借助下面的通用的映射类型工具,在输入类型做了约束,必须要包含 king 且类型为 string 的属性,在遍历 Events 的时候使用 as 取 E 中名为 kind 的 value 作为新类型的 key。
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
复制代码
我们使用一下这个类型工具:
type Config = EventConfig<SquareEvent | CircleEvent>
// ^?
复制代码
去演练场验证答案
Template Literal Types:
模板字符类型的语法同 JavaScript 中模板字符串,但使用的位置不同,模板字符类型应用在类型位置。通过使用模板类型来扩展/链接内容创建新的字符类型。
入门案例:HelloWorld
下面使我们的入门案例,通过模板插值将类型 World 插入到了类型 Greeting 中,最终创建的类型为“hello world”,注意这里面的“hello world”是类型而不是值。
type World = "world";
type Greeting = `hello ${World}`;
// ^?
复制代码
去演练场验证答案
案例-插值位置使用联合类型:
下面的案例可以得到一个结果,当插值位置是用联合类型是,结果将是由每个联合成员便是的每个可能的字符串组成的集合。
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// ^?
复制代码
去演练场验证答案
案例-多个插值位置使用联合类型:
下面的案例可以得到一个结果,当每个插值位置均使用联合类型时,结果的数量将是每个联合元素个数相乘的积。
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// ^?
复制代码
去演练场验证答案
案例-在类型中使用字符串联合
案例介绍:
我们通常会在做一系列操作的函数前面定义一个明显的前缀;
在这个案例中实现监听对象属性的变化;
需要有一个 on 函数来接收,监听事件的名称我们做特定的约束格式为:key+Changed;callback 是一个没有返回值的函数。
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
复制代码
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
// Prevent easy human error (using the key instead of the event name)
person.on("firstName", () => {});
// It's typo-resistant
person.on("frstNameChanged", () => {});
复制代码
去演练场验证答案
案例-模板字符类型推导
这个案例是上一个案例的升级版本,增加了对返回类型的推导,使得这个类型工具将更加的完善。
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
});
person.on("firstNameChanged", newName => {
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
if (newAge < 0) {
console.warn("warning! negative age");
}
})
复制代码
去演练场验证答案
整理内置的模板字符操作类型:
将每个字符均转为大写:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
// ^?
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
// ^?
复制代码
去演练场验证答案
将每个字符均转为小写:
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// ^?
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
// ^?
复制代码
去演练场验证答案
将第一个字符转为大写
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// ^?
复制代码
去演练场验证答案
将第一个字符转为小写:
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// ^?
复制代码
去演练场验证答案
写到最后:
至此 TypeScript 类型编程的 7 大块内容就已经过了一遍了,模板字符类型的案例还需要多熟悉熟悉。在官网还有一些提供的内容类型工具可以直接供我们在实际开发中使用,这里给出Utility Types的地址方便大家查询。类型编程和我们以往的编程一样,同样在乎基础的学习和大量的练习。上次推荐的开源类型挑战项目type-challenges你有练习打卡吗?
团队介绍
高灯科技交易合规前端团队(GFE), 隶属于高灯科技(北京)交易合规业务事业线研发部,是一个富有激情、充满创造力、坚持技术驱动全面成长的团队, 团队平均年龄 27 岁,有在各自领域深耕多年的大牛, 也有刚刚毕业的小牛, 我们在工程化、编码质量、性能监控、微服务、交互体验等方向积极进行探索, 追求技术驱动产品落地的宗旨,打造完善的前端技术体系。
愿景: 成为最值得信任、最有影响力的前端团队
使命: 坚持客户体验第一, 为业务创造更多可能性
文化: 勇于承担、深入业务、群策群力、简单开放
Github:github.com/gfe-team
团队邮箱:gfe@goldentec.com
作者:GFE-小鑫同学著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
评论