TS 高级功法
开篇
金庸笔下的武侠功法秘籍不胜枚举,主角凭借着至高无上的功法自在行走于江湖。对于我们前端工程师来说如何轻松驾驭日常开发呢?势必不可缺少 TS 技能。
TS 作为 JS 的严格静态类型检查工具,意味着在编码阶段,可以及早发现可能带入生产环境的隐患。
所以,对于前端开发,这本 TS 秘籍必不可少。接下来我们一起来看看 TS 的一些高级用法。
一、必备知识
1、联合类型(Union Type)
- 概念:
联合类型是由两种或多种其他类型组合而成的类型,表示该值的类型是这些类型之一。TS 中使用
|
操作符来创建联合类型。如将 number 和 string 组合成新的类型:
type NumOrStr = number | string;
const str: NumOrStr = 'abc';
const num: NumOrStr = 123;
- 场景: 一个函数的参数变量支持传入单个数据和多条数据,我们可以通过联合类型来支持这样场景:
function greet(person: string | string[]): string | string[] {
if (typeof person === 'string') {
return `Hello ${person}`;
} else if (Array.isArray(person)) {
return person.map(name => `Hello ${name}`);
}
}
对于参数变量,由于定义为联合类型,我们只能访问 string 和 array 的共有属性
如:indexOf 等。
这里我们使用了 typeof
操作符对联合类型进行缩窄变量的类型范围,使得变量类型只能为一种情况。
2、泛型变量
TS 泛型提供了类型定义的复用及灵活性,一个简单的泛型定义如下:
function identity<T>(value: T): T {
return value;
}
identity<string>('name');
上面 T
就是一个泛型变量,它是定义在函数中的类型占位符:将用户指定的实际类型,链式传递给参数类型和返回值类型。
泛型变量可以是任意字母,常见的泛型变量定义有:
- T(Type)代表任意事物的类型;
- K(Key)表示对象中键的类型;
- V(Value)表示对象中值的类型;
- E(Element)表示元素类型;
当未指定实际类型时,TS 会根据参数推导出类型,并应用于 return 返回值,如我们实现一个
clone
函数,使得返回克隆后的值类型和目标值类型保持一致。
export function checkedType(target: any) {
return Object.prototype.toString.call(target).slice(8, -1) //返回数据类型
}
export function clone<T>(target: T) {
let result: any, targetType = checkedType(target);
if (targetType === 'Object') {
result = {};
} else if (targetType === 'Array') {
result = [];
} else {
return target;
}
for (let i in target) {
let value = target[i];
if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
result[i] = clone(value);
} else {
result[i] = value;
}
}
return result as T;
}
3、交叉类型
通过交叉运算符 &
对多个类型进行交叉合并,新类型拥有所有类型的成员。
比如,两个 interface 对象类型的到的交叉类型:
interface Point {
x: number;
y: number;
c: number;
}
interface Named {
name: string;
c: string;
}
Point & Named -->
{
x: number;
y: number;
name: string;
c: never;
}
由于 Point 和 Named 都有一个公共类型成员 c,但由于 c 在二者中类型分别是 number 和 string,交叉后类型是 never。
4、any 类型 和 unknown 类型
- any 类型,这和没有引入 TS,纯 JS 使用体验没有差别;你可以将任意类型值赋给 any 类型的变量,并对该变量进行任何操作。
- unknown 类型,你可以把任意类型值赋给 unknown 类型的变量,但在使用这个变量时必须进行类型检查或类型断言。
为了保证类型安全,我们应当尽可能使用 unknown
类型。
function invokeCallback(callback: unknown) {
if (typeof callback === 'function') {
callback();
}
}
5、interface 与 type
-
类型别名
type
: 用于给一个或一组类型(包括基本类型、联合类型、接口类型)起一个新名字。我们常见的泛型工具函数(如:Pick
)都是采用 type 类型别名 定义。 -
接口类型
interface
: 只能用于定义对象、或者函数类型,定义它们自身的属性和方法。
- 相同点:
- 两者都可以用来描述对象和函数;
- 两者都支持扩展,类型别名使用
&
扩展组合多个类型,interface 通过extends
方式扩展;
- 不同点:
- 类型别名更灵活,基本类型、联合类型、元素类型都可以,而 interface 不行;
- 同名的两个定义类型,interface 会自动合并类型内容,而类型别名不会;
举例:当一个参数存在多种类型时,type 比 interface 更适合使用,比如一个创建文件接口,既可以根据文件 url
在线地址创建,也可以根据文件 FormData
来创建:
type TCreateFileParams = {
url: string;
} | FormData
6. 映射类型
TS 提供了一种将 string | number | symbol 或 字面量的 联合类型
作为 key 映射为一个新的对象结构类型。
type Keys = 'name' | 'sex';
type User = {
[K in Keys]: string;
}
// 等同于
type User = {
name: string;
sex: string;
}
需要注意的是这个语法描述的是类型而非成员。若想添加额外的成员,需使用交叉类型:
type Keys = 'name' | 'sex';
type User = {
// age: number; // 不要这样扩展新成员
[K in Keys]: string;
} & { age: number }; // 推荐这样使用
7. 模块的 export 和 import 复合写法
如果在一个模块中,先输入后输出同一个模块,import 语句可以和 export 语句写在一起,比如 antd 将所有组件整合在 index.ts 中进行导出。
- 如果是导出类型,可以在 export 后面增加 type 来标识要导出 TS 类型声明;
- 如果是导出组件、枚举类型以及其他非类型的 export 模块,不需要在 export 后面增加 type。
// 导入导出类型
export type { FormInstance, FormProps, FormItemProps } from './form';
// 导出模块、枚举变量
export { default as Form, EFormType } from './form';
二、extends 关键字
extends
在 TS 中有三种用法:接口继承、条件判断 以及 泛型约束。
1. 接口继承
-
场景: 我们在编写代码时通常会将公共部分进行抽离,在需要用到的地方引入复用。代码是如此,TS 类型同样可以这样。
-
解释: 对于可重用的类型,我们会选择将其分割到一个独立的接口中,其他模块则通过继承来拥有这部分类型。
-
使用: 教师和学生都用姓名、性别和年龄等公共属性,除此之外,教师拥有
「所属办公室」
属性,学生拥有「所属班级」
属性,我们可以这样定义类型:
interface IBaseInfo {
name: string;
sex: string;
age: number;
}
interface ITeacher extends IBaseInfo {
office: string; // 办公室
}
interface IStudent extends IBaseInfo {
classroom: string; // 教室
}
2. 条件判断
-
解释:
extends
当用作条件判断时,它的语句和 JS 三目运算符 很像,在满足条件时返回 ? 后面的类型,否则返回 : 后面的类型。 -
普通用法:
interface A1 {
name: string;
}
interface A2 {
name: string;
age: number;
}
type Value = A2 extends A1 ? string : number;
const value: Value = 'is string';
extends
判断条件真假的逻辑:extends 前面的类型能够赋值给 extends 后面的类型,则表达式为真,否则为假。
由于 A2 的接口一定可以满足 A1,所以条件为真。
- 泛型用法: 假如还有 A3、A4 ... 都去使用 extends 与 A1 判断就会编写很多代码;泛型的灵活性可以简化这个工作。
type P<T> = T extends A1 ? string : number;
type Value = P<A2>; // string
再来看一个复杂例子:如果泛型传递的是「联合类型」,且判断逻辑可能是 true 也可能是 false。这时 TS 只能将两个结果的值联合起来都返回。(分配条件类型)
type P<T> = T extends 'x' ? 'a' : 'b';
type Value = P<'x' | 'y'>; // 'a' | 'b'
- 应用场景:
我们可以借助 extends 泛型 + 联合类型的特性,实现一个
Diff
泛型工具类型。
type Filter<T, U> = T extends U ? never : T;
type Values = Filter<'x' | 'y' | 'z', 'x'>; // 'y' | 'z'
3. 泛型约束
泛型指的是在定义函数/接口/类型时,不预先指定具体的类型,而是在使用的时候再指定具体类型的一种特性。
但有时候我们希望泛型变量可以精确到某一类类型,extends
可以用来约束泛型变量的类型范围。下面这个例子要求参数必须满足 Person
成员属性:
interface Person {
name: string;
age: number;
}
const student = <T extends Person>(data: T): T => {
return data;
}
student({ name: 'cegz' }); // error. Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.ts(2345)
student({ name: 'cegz', age: 24 }); // success
student({ name: 'cegz', age: 24, sex: 'male' }); // success
比如 React.lazy
它会限制必须返回 Promise,其中的 T
约束必须是一个 React 组件。
function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;
三、infer 关键字
infer
通过模式匹配的方式,可以提取 TS 目标类型的某一部分进行返回,与 ES6 解构类似。
模式匹配
是指:通过一个类型和匹配规则,将需要提取的部分通过 infer 声明一个局部变量
,这个局部变量就是提取到的类型。
- 提取元组类型中最后一个元素类型:
type Last<Arr extends unknown[]> =
Arr extends [...infer rest,infer Ele]
? Ele
: never;
type Value = Last<[1, 2, 3]>; // 3
如果我们想明确最后一个元素类型为 number,可以使用 infer extends
语法来约束推导的类型:
type Last<Arr extends unknown[]> =
Arr extends [...infer rest,infer Ele extends number]
? Ele
: never;
- 提取函数的返回值作为类型:
type GetReturnType<Func extends Function> =
Func extends (...args: any[]) => infer ReturnType
? ReturnType
: never;
type Result = GetReturnType<() => 'return string.'>
- 枚举联合类型的 key:
type Name = { name: string };
type Age = { age: number };
type Union = Name | Age;
type UnionKey<P> = P extends infer T ? keyof T : never;
type T = UnionKey<Union>; // 'name' | 'age'
infer 表示待推断类型,功能非常强大,可以在任意位置代指其类型,配合 extends 一起使用,使得类型推导具备一定的编程能力。
四、typeof 操作符
typeof
在 JS 中用于检测数据类型,而在 TS 中,typeof 可以根据一个 JS 变量、对象或者函数的推导出其类型。
- 基础变量: 当 typeof 检测一个基本类型值,会得到值所属的基本类型;当 typeof 的值是一个 const 常量时,得到的则是值的字面量。
let unknownString = 'cegz';
type S = typeof unknownString; // string
const unknownString = 'cegz';
type S = typeof unknownString; // 'cegz'
// 等同于:
let unknownString = 'cegz' as const;
- 对象:
const info = {
title: '信息',
num: 24
}
type Info = typeof info;
// 得到:
type Info = {
title: string;
num: number;
}
如果使用 const 断言,结果会大不一样:
const info = {
title: '信息',
num: 24
} as const
type Info = typeof info;
// 得到:
type Info = {
readonly title: "信息";
readonly num: 24;
}
const 断言
会构造字面量值,并且字面量属性都使用 readonly
修饰。
- 函数:
typeof 用于函数时得到一个函数签名。
const Fn = (value: string) => value;
type F = typeof Fn;
// 得到:
type F = (value: string) => string;
- 数组:
当 typeof 用于一个数组时,结合 const
断言,会将数组中的每一项,转换为一个联合类型。
const Placements = [
'topLeft', 'topCenter', 'topRight', 'bottomLeft', 'bottomCenter', 'bottomRight'
] as const;
type Placement = typeof Placements[number]; // 关键 [number]
// 等价于
type Placement = "topLeft" | "topCenter" | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
五、keyof 关键字
keyof
是索引类型查询操作符,获取索引类型的属性名,构成联合类型。
interface Point {
x: number;
y: number;
}
type P = keyof Point; // 'x' | 'y'
现在,我们定义一个 getProperty
方法用来返回对象的属性,借助泛型、extends 以及 keyof,实现如下:
function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = {
id: '1',
name: 'xiaoming'
}
const userId = getProperty(user, 'id'); // 1
const userName = getProperty(user, 'name'); // xiaoming
const userAge = getProperty(user, 'age'); // error. Argument of type '"age"' is not assignable to parameter of type '"id" | "name"'.
typeof 操作符可以用来获取一个变量或对象的类型,而 keyof 操作符可以获取某种类型的所有键,返回其联合类型。二者可以结合使用:
const propertys = Object.keys(user).map(key => user[key as keyof typeof user]);
console.log(propertys); // [ '1', 'xiaoming' ]
除此之外,它还可以用于联合枚举类型的 key:
enum EStatus {
success,
warning,
error,
}
type TStatusKey = keyof typeof EStatus;
// 得到:
type TStatusKey = "success" | "warning" | "error";
六. in 关键字 - 类型保护
有时候我们定义的变量是一个联合类型,它存在多种类型,当想使用具体某一个类型时,一般可以选择 as
断言,还有一种方式则是通过 in
操作符操作属性
进行类型保护:
const changeDesc = (data: { desc: string } | { guide_desc: string }) => {
// (data as { desc: string }).desc; // 一种是断言
if ('desc' in data) { // 一种是使用 in 操作符
data.desc
} else {
data.guide_desc
}
}
七、特殊符号
合理灵活地运用「TS 特殊符号」可以简化我们的程序逻辑和代码量,一定程度上提升代码的易读性。
1. ?.(可选链)
通常用作访问对象的可选属性。在遇到 null 或 undefined 时可以立即停止表达式的运行。
比如我们有一个 Info
接口和一个 info
对象,其中 desc
又是一个对象,但它是个可选属性:
interface Info {
title: string;
desc?: {
'zh-cn': string;
'en': string;
};
}
const info: Info = {
title: '信息',
}
如果我们直接访问 desc
下的属性会报错:
console.log(info.desc.en); // error TS2532: Object is possibly 'undefined'.
上面错误意思是:对象是个 undefined;这时我们可以使用 可选链 ?.
操作符来解决报错,它会在遇到 undefined 时停止表达式的执行,并返回 undefined:
console.log(info.desc?.en); // undefined
再比如有一组 list,你需要查找其中的某一项,你也可以使用可选链操作符来减少逻辑判断:
const list = [
{ id: 1, desc: 'first' },
{ id: 2, desc: 'second' },
{ id: 3, desc: 'thrid' },
];
const desc = list.find(v => v.id === 3)?.desc || ''; // const desc: string
2. !.(非空类型断言)
它的含义是:确保某个可选值🕐是有值的。它不会像 ?.
那样遇到空值返回 undefined,而是类似 类型断言
:尽管这个属性是个可选值,但在这里我认为它是一定有值的。
function func(value?: string) {
value!.length
}
3. ??(空值合并运算符)
这个容易理解:当左侧操作数为 null 或 undefined 时,其返回右侧的操作值,否则返回左侧的操作值。
let myName: string | undefined = undefined;
myName ?? 'cegz'
八、模板字面量类型
在 TS 4.1 中引入了模板字面量类型,类似于 ES6 模板字符串,在 TS 场景下字面量类型中可以使用类型变量,类型变量的类型为:string、number、boolean、bigint。
通过使用模板字面量类型,在一定场景下可以简化我们的类型定义,减少重复代码。
假设我们定义 CSS padding 和 margin 属性:
type CssPadding =
| "padding-left"
| "padding-right"
| "padding-top"
| "padding-bottom";
type CssMargin =
| "margin-left"
| "margin-right"
| "margin-top"
| "margin-bottom";
其中 left、right、top、bottom
为重复定义内容。下面我们使用「模板字面量类型」来简化重复内容:
type Direction = "left" | "right" | "top" | "bottom";
type CssPadding = `padding-${Direction}`;
type CssMargin = `margin-${Direction}`;
九、函数类型重载
当定义一个函数,它的参数和返回值拥有多种可能性时,通过 类型重载
来定义复数类型,逐个匹配第一个满足条件的类型。
如:React.createElement
支持接收 HTML 元素、FunctionComponent、ClassComponent 作为参数,并返回不同的类型,函数重载可以解决这类问题,提高代码可读性。
function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
type: keyof ReactHTML,
props?: ClassAttributes<T> & P | null,
...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
function createElement<P extends {}>(
type: FunctionComponent<P>,
props?: Attributes & P | null,
...children: ReactNode[]): FunctionComponentElement<P>;
function createElement<P extends {}>(
type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
props?: ClassAttributes<ClassicComponent<P, ComponentState>> & P | null,
...children: ReactNode[]): CElement<P, ClassicComponent<P, ComponentState>>;
十、interface 定义函数
通常,我们可以采用 type
来定义函数签名类型:
type Fn = (name: string) => string;
但在一些情况下,我们除了定义函数体类型外,还想定义函数上的自身属性,可以使用 interface
定义,比如 React.FunctionComponent
:
interface FunctionComponent<P = {}> {
// 定义函数签名
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
// 定义函数自身属性
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
十一、类型声明文件 d.ts
在 TS 没有流行之前,大多数库都是 JS 编写的,这导致在 TS 环境下使用这些库,会看到一连串找不到类型的提示。
“d.ts” 文件用于解决这类问题,在不用重构 JS 代码的情况下,为 JavaScript 编写 API 对应的 TS 类型信息。
TS 默认会查找 node_modules/@types
下是否有相关库类型定义,比如 react
它的类型定义是:@types/react
,它下面的 d.ts
文件存放了 React 类型定义。
当然,有时候我们并不想为当前包提供类型,去多创建一次 npm 发包到线上;可以通过在当前包下编写 .d.ts 类型定义,并在 package.json
中声明类型定义位置:
// package.json
{
...
"typings": "./types/index.d.ts"
}
再比如,我们需要在工程下为 window 定义全局变量,第一步要在 tsconfig.json
中引入类型声明文件:
// tsconfig.json
{
...
"include": [
"src/**/*",
"./@types/**/*" // 配置的.d.ts文件
],
}
第二步,declare
可用于声明全局变量,通过 declare global
扩展 Window 属性类型定义:
// @types/global.d.ts
// 在 .d.ts 配置文件中编写 global 属性时,要先进行 export
export {};
declare global {
// 全局变量声明
function i18n(text: string): string;
interface Window {
language: string;
}
}
此外,我们还可以使用 declare
声明 .css、.scss、.png
等文件模块,解决 TS 环境下引入模块时的无法识别问题:
declare module '*.module.css' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
十二、常见泛型工具类型
1. Partial
Partial
可以将一个已有的 interface 接口属性成员,变为可选类型,简化我们定义新的接口来为成员加上可选修饰符的工作。
使用示例:
interface IUser {
name: string;
avatar: string;
contacts: {
name: string;
age: number;
}
}
type PartialUser = Partial<IUser>;
得到类型如下:
type PartialUser = {
name?: string | undefined;
avatar?: string | undefined;
contacts?: {
name: string;
age: number;
} | undefined;
}
从上我们可以看出 Partial
只对第一层属性成员添加可选修饰符,具体实现如下:
// 浅处理
type ShallowPartial<T> = {
[K in keyof T]?: T[K];
}
// 深处理(递归)(一个函数也满足 `extends object`,所以前置条件先判断是否满足 `extends Function`。)
type DeepPartial<T> = T extends Function
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Partial
的实现理解了,相信你会推断出 Readonly
(将属性成员变成只读)、Required
、的实现。
2. Record
Record
用于构建一个新的 interface 类型,接收两个泛型变量:
- 第一个泛型变量 K,可以是字面量类型或联合类型,指定新类型的成员属性;
- 第二个泛型变量 T,是每一个成员属性的类型定义。
通常用于处理一组相同类型的对象属性非常有用。使用示例:
interface PageInfo {
title: string;
}
type Page = 'home' | 'about' | 'contact';
const x: Record<Page, PageInfo> = {
about: { title: 'about' },
contact: { title: 'contact' },
home: { title: 'home' },
};
实现如下:
type ShallowRecord<K extends (string | number | symbol), T> = {
[P in K]: T;
}
3. Pick
Pick
用于从一个类型中挑选出部分属性,来构造出一个新的 interface 类型。
使用示例:
interface IUser {
name: string;
age: string;
avatar: string;
}
type UserPreview = Pick<IUser, 'name' | 'avatar'>;
const IUser: UserPreview = {
name: 'cegz',
avatar: 'avatar.png',
};
实现如下:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
4. Exclude
Exclude
排除,从字面量联合类型 T 中剔除所有可以赋值给 U 的属性,然后构造一个新的类型。
使用示例:
type T = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
实现如下:
type Exclude<T, U> = T extends U ? never : T;
5. Omit
Omit
与 Pick
相反,用于从类型 T 中剔除部分属性 K 来构造类型。
使用示例:
interface IUser {
name: string;
age: string;
avatar: string;
}
type TUser = Omit2<IUser, 'age'>;
// 得到:
type TUser = {
name: string;
avatar: string;
}
结合 Pick
和 Exclude
,实现如下:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
6. ReturnType
ReturnType
用于提取一个函数类型的返回值,作为新的类型返回。
使用示例:
type R = ReturnType<() => string>; // string
实现如下:
type ReturnType<T extends (...args: any[]) => any> = // 约束 T 必须是一个函数
T extends (...args: any[]) => infer R ? R : any;
7. Parameters
Parameters
用于提取函数的参数类型,并形成一个元组类型。
使用示例:
function greet(name: string) {
return `Hello, ${name}.`;
}
type TParams = Parameters<typeof greet>;
const params: TParams = ['xiaoming'];
实现如下:
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
可以看到,ReturnType
和 Parameters
结合 typeof
关键字,可以很轻松的对一个 JS 函数进行类型拆解。
十三、编写复杂 TS 类型
有了上面的前置知识,我们来看一道力扣 TS 编程题:编写复杂的 TypeScript 类型。
在开始这点编程题之前,我们先来思考下面两个问题。
1. 如何将非函数的属性去掉?
假设我们现有如下对象,对象自身具有属性和方法:
const EffectModule = {
message: '',
setMessage() {
return this.message;
},
}
我们如何过滤掉对象属性,只保留对象的方法来构造成一个新的类型?
首先第一步,定义一个 PickFuncKeys
泛型,用于收集对象的方法并组成一个字面量联合类型,实现如下:
type PickFuncKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T]; // [keyof T] 用在这里怎么就可以将 interface 类型转变成 key 字面量联合类型?哪位同学可以解答~
第二步,借助 Pick
工具函数,根据 PickFuncKeys
收集到的函数属性字面量生成一个新的 interface 类型。PickFunc
的实现如下:
type PickFunc<T> = Pick<T, PickFuncKeys<T>>;
我们应用到示例中:
type E = PickFunc<typeof EffectModule>;
// 得到:
type E = {
setMessage: () => string;
}
2. 如何转换函数类型签名?
假设我们现有如下函数,从:
type Fn<T, U> = (arg: Promise<T>) => Promise<U>;
变为:
(arg: T) => U;
要实现上述操作,核心点在于:提取 Promise
泛型中的变量。
我们先来实现一个简单操作:提取函数的参数类型,借助 infer
可以轻松实现:
type ParamsType<T> = T extends (params: infer P) => any ? P : T;
type Params = ParamsType<Fn<string, number>>; // Params = Promise<string>
现在,如果我们想要拿到 Promise
中的泛型变量,可以这样实现:
type ExtractPromise<T> =
T extends (arg: Promise<infer T>) => Promise<infer U>
? (arg: T) => U
: never;
type PromiseType = ExtractPromise<Fn<string, number>>; // PromiseType = (arg: string) => number
现在,我们来看看这道思考题,来加深对上面两个操作的印象。
3. 编程题
假设有一个 EffectModule
类,它上面会有属性、同步方法 setMessage
和 异步方法 delay
,定义如下:
interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>): Promise<Action<string>> {
return input.then((i) => ({
payload: `hello ${i}!`,
type: "delay",
}));
}
setMessage(action: Action<Date>): Action<number> {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message",
};
}
}
现要求根据 EffectModule
的类型,构造出一个仅包含方法的类型,并且同步方法和异步方法签名由上面的定义变成如下所示:
type NewType = {
delay(input: number): Action<string>;
setMessage(action: Date): Action<number>;
};
现在,我们要对 EffectModule
实例上的函数签名进行修改,并过滤掉非函数属性,返回一个新类型。结合上面的知识,代码实现如下:
type PickFuncKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type ExtractContainer<P> = {
[K in PickFuncKeys<P>]: // 首先过滤掉非函数属性
P[K] extends (arg: Promise<infer T>) => Promise<infer U> ? (arg: T) => U : // 对 Promise 类型方法的处理
P[K] extends (arg: Action<infer T>) => Action<infer U> ? (arg: T) => Action<U> : // 对 Action 类型当法的处理
never
}
type NewType = ExtractContainer<EffectModule>;
// 得到:
type NewType = {
delay: (arg: number) => Action<string>;
setMessage: (arg: Date) => Action<number>;
}
最后
参考: 1. 快速掌握 TypeScript 新语法:infer extends 2. 考察 TypeScript 3. 精读《@types react 值得注意的 TS 技巧》 4. TS 动画版进阶教程 5. TypeScript进阶之工具类型&高级类型 6. 想去力扣当前端,TypeScript 需要掌握到什么程度?
转载自:https://juejin.cn/post/7218389542586482744