likes
comments
collection
share

TS 高级功法

作者站长头像
站长
· 阅读数 54

开篇

金庸笔下的武侠功法秘籍不胜枚举,主角凭借着至高无上的功法自在行走于江湖。对于我们前端工程师来说如何轻松驾驭日常开发呢?势必不可缺少 TS 技能。

TS 作为 JS 的严格静态类型检查工具,意味着在编码阶段,可以及早发现可能带入生产环境的隐患。

所以,对于前端开发,这本 TS 秘籍必不可少。接下来我们一起来看看 TS 的一些高级用法。

一、必备知识

1、联合类型(Union Type)

  1. 概念: 联合类型是由两种或多种其他类型组合而成的类型,表示该值的类型是这些类型之一。TS 中使用 | 操作符来创建联合类型。如将 number 和 string 组合成新的类型:
type NumOrStr = number | string;
const str: NumOrStr = 'abc';
const num: NumOrStr = 123;
  1. 场景: 一个函数的参数变量支持传入单个数据和多条数据,我们可以通过联合类型来支持这样场景:
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: 只能用于定义对象、或者函数类型,定义它们自身的属性和方法。

  1. 相同点:
  • 两者都可以用来描述对象和函数;
  • 两者都支持扩展,类型别名使用 & 扩展组合多个类型,interface 通过 extends 方式扩展;
  1. 不同点:
  • 类型别名更灵活,基本类型、联合类型、元素类型都可以,而 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. 接口继承

  1. 场景: 我们在编写代码时通常会将公共部分进行抽离,在需要用到的地方引入复用。代码是如此,TS 类型同样可以这样。

  2. 解释: 对于可重用的类型,我们会选择将其分割到一个独立的接口中,其他模块则通过继承来拥有这部分类型。

  3. 使用: 教师和学生都用姓名、性别和年龄等公共属性,除此之外,教师拥有「所属办公室」属性,学生拥有「所属班级」属性,我们可以这样定义类型:

interface IBaseInfo {
  name: string;
  sex: string;
  age: number;
}

interface ITeacher extends IBaseInfo {
  office: string; // 办公室
}

interface IStudent extends IBaseInfo {
  classroom: string; // 教室
}

2. 条件判断

  1. 解释: extends 当用作条件判断时,它的语句和 JS 三目运算符 很像,在满足条件时返回 ? 后面的类型,否则返回 : 后面的类型。

  2. 普通用法:

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,所以条件为真。

  1. 泛型用法: 假如还有 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'
  1. 应用场景: 我们可以借助 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 声明一个局部变量,这个局部变量就是提取到的类型。

  1. 提取元组类型中最后一个元素类型:
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;
  1. 提取函数的返回值作为类型:
type GetReturnType<Func extends Function> = 
    Func extends (...args: any[]) => infer ReturnType 
        ? ReturnType 
        : never;
        
type Result = GetReturnType<() => 'return string.'>
  1. 枚举联合类型的 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 变量、对象或者函数的推导出其类型。

  1. 基础变量: 当 typeof 检测一个基本类型值,会得到值所属的基本类型;当 typeof 的值是一个 const 常量时,得到的则是值的字面量。
let unknownString = 'cegz';
type S = typeof unknownString; // string

const unknownString = 'cegz';
type S = typeof unknownString; // 'cegz'
// 等同于:
let unknownString = 'cegz' as const;
  1. 对象:
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 修饰。

  1. 函数:

typeof 用于函数时得到一个函数签名。

const Fn = (value: string) => value;
type F = typeof Fn;

// 得到:
type F = (value: string) => string;
  1. 数组:

当 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

OmitPick 相反,用于从类型 T 中剔除部分属性 K 来构造类型。

使用示例:

interface IUser {
  name: string;
  age: string;
  avatar: string;
}
type TUser = Omit2<IUser, 'age'>;

// 得到:
type TUser = {
  name: string;
  avatar: string;
}

结合 PickExclude,实现如下:

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;

可以看到,ReturnTypeParameters 结合 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
评论
请登录