业务开发所需的 TypeScript 常用技巧

1,845 阅读4分钟

React

React.FC

import React, { HTMLAttributes, PropsWithChildren } from "react";

interface IHelloProps extends HTMLAttributes<HTMLDivElement> {
  name: string;
}

const Hello: React.FC<PropsWithChildren<IHelloProps>> = ({
  name,
  children,
  ...rest
}) => {
  return (
    <div>
      <div {...rest}>{`Hello, ${name}!`}</div>
      {children}
    </div>
  );
};
  1. 使用 PropsWithChildrenIHelloProps 注入 children 类型
  2. 使用 React.FC 声明组件,通过泛型参数传入组件 Props 类型
    • 注意: react@16 类型定义中 React.FC 自带 children 类型,无需额外处理(即可省略第 1 步)
  3. 若组件需要接受 html 属性,如 classNamestyle 等,可以直接 extends HTMLAttributes<HTMLDivElement>,其中 HTMLDivElement 可替换为所需要的类型,如 HTMLInputElement

不推荐 React.FC?

Remove React.FC from Typescript template #8177

在这个 PR 里移除了 CRA 默认模板的 React.FC,主要有以下几点理由:

  1. 隐式定义了 children
  2. 无法支持泛型组件
  3. 挂载静态属性较为复杂,如 <Select.Option>
  4. defaultProps 存在问题

好处只有一点:

  1. 提供了返回值约束

所以是否使用 React.FC 可以自行选择,泛型组件、挂载静态属性属于低频场景,遇见了不用 React.FC 就是了~

defaultProps 基本已经不会在业务代码中使用(使用默认值替代),并且最新的 React.FC 已经移除了内置 children

如果对于返回值有明确的类型要求,配置了 TypeScript 规则,那么可以使用 React.FC,其他时候可以直接定义 Props,如下所示:

import React, { HTMLAttributes, PropsWithChildren } from "react";

interface IHelloProps extends HTMLAttributes<HTMLDivElement> {
  name: string;
}

const Hello = ({ name, children, ...rest }: PropsWithChildren<IHelloProps>) => {
  return (
    <div>
      <div {...rest}>{`Hello, ${name}!`}</div>
      {children}
    </div>
  );
};

React.forwardRef

React 提供了 forwardRef 函数用于转发 Ref,该函数也可传入泛型参数,如下:

import { forwardRef, PropsWithChildren } from "react";

interface IFancyButtonProps {
  type: "submit" | "button";
}

export const FancyButton = forwardRef<
  HTMLButtonElement,
  PropsWithChildren<IFancyButtonProps>
>((props, ref) => (
  <button ref={ref} className="MyClassName" type={props.type}>
    {props.children}
  </button>
));

React.ComponentProps

用于获取组件 Props 的工具泛型,与之类似的还有:

  • React.ComponentPropsWithRef
  • React.ComponentPropsWithoutRef
import { DatePicker } from "@douyinfe/semi-ui";

type SemiDatePikerProps = React.ComponentProps<typeof DatePicker>;

export const DisabledDatePicker: React.FC = () => {
  const disabledDate: SemiDatePikerProps["disabledDate"] = (date) => {
    // ...
  };

  return <DatePicker disabledDate={disabledDate} />;
};

使用第三方库组件时,不要使用具体 path 去引用类型(若第三方组件后续升级修改了内部文件引用路径,会出现错误)。

import { InputProps } from "@douyinfe/semi-ui/input"; // ×

import { InputProps } from "@douyinfe/semi-ui"; // √

若入口文件未暴露对应组件的相关类型声明,使用 React.ComponentProps

import { Input } from "@douyinfe/semi-ui";

type InputProps = React.ComponentProps<typeof Input>;

另外一个例子:

image.png

类型收窄

某些场景传入的参数为联合类型,需要基于一些手段将其类型收窄(Narrowing)。

function printAll(strs: string | string[] | null) {
  if (strs && typeof strs === "object") {
    // strs 为 string[]
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    // strs 为 string
    console.log(strs);
  }
}

使用 type predicates: is

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

思考一下 Lodash 的 isBoolean/isString/isArray...等函数,再思考一下使用 isEmpty 有什么不对。

interface LoDashStatic {
  isBoolean(value?: any): value is boolean;
  isString(value?: any): value is string;
  isArray(value?: any): value is any[];
  isEmpty(value?: any): boolean; // 这里的定义会使得业务中时使用出现什么问题?
}

类型安全的 redux action

笔者不用 redux,此处仅做演示

TS Playground - An online editor for exploring TypeScript and JavaScript

interface ActionA {
  type: "a";
  a: string;
}

interface ActionB {
  type: "b";
  b: string;
}

type Action = ActionA | ActionB;

function reducer(action: Action) {
  switch (action.type) {
    case "a":
      return console.info("action a: ", action.a);
    case "b":
      return console.info("action b: ", action.b);
  }
}

reducer({ type: "a", a: "1" }); // √
reducer({ type: "b", b: "1" }); // √

reducer({ type: "a", b: "1" }); // ×
reducer({ type: "b", a: "1" }); // ×

多参数类型约束

以非常熟悉的 window.addEventListener 为例:

// e 为 MouseEvent
window.addEventListener("click", (e) => {
  // ...
});

// e 为 DragEvent
window.addEventListener("drag", (e) => {
  // ...
});

可以发现 addEventListener 的回调函数入参类型(event)会随着监听事件的不同而不同,addEventListener 的函数签名如下:

addEventListener<K extends keyof WindowEventMap>(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;

type 为泛型 K,约束在 WindowEventMap 的 key 范围内,再基于 K 从 WindowEventMap 推导出 ev 事件类型即可。

当然你也可以选择使用联合类型,就像 redux action 那样。

常用工具泛型

了解完 TypeScript 基础内容(keyof/in/extends/infer)后,可自行尝试实现内置工具泛型,实现一遍理解更深刻。

interface Person {
  name: string;
  age: number;
  address?: string;
}
  • Partial。将所有字段变为 optional
type PartialPerson = Partial<Person>;
// ↓
type PartialPerson = {
  name?: string | undefined;
  age?: number | undefined;
  address?: string | undefined;
};
  • Required。将所有字段变为 required
type RequiredPerson = Required<Person>;
// ↓
type RequiredPerson = {
  name: string;
  age: number;
  address: string;
};
  • Pick<T, K extends keyof T>。从 T 中取出部分属性 K
type PersonWithoutAddress = Pick<Person, "name" | "age">;
// ↓
type PersonWithoutAddress = {
  name: string;
  age: number;
};
  • Omit<T, K extends keyof T>。从 T 中移除部分属性 K
type PersonWithOnlyAddress = Omit<Person, "name" | "age">;
// ↓
type PersonWithOnlyAddress = {
  address?: string | undefined;
};
  • Exclude<T, U>。从 T 中排除那些可分配给 U 的类型

该泛型实现需要掌握 Distributive Conditional Types

type T = Exclude<1 | 2, 1 | 3>; // -> 2
  • Extract<T, U>。从 T 中提取那些可分配给 U 的类型

该泛型实现需要掌握 Distributive Conditional Types

type T = Extract<1 | 2, 1 | 3>; // -> 1
  • Parameters。获取函数入参类型
declare function f1(arg: { a: number; b: string }): void;

type T = Parameters<typeof f1>;
// ↓
type T = [
  arg: {
    a: number;
    b: string;
  }
];
  • ReturnType。获取函数返回值类型
declare function f1(): { a: number; b: string };

type T = ReturnType<typeof f1>;
// ↓
type T = {
  a: number;
  b: string;
};
  • Record<K, T>。将 K 中所有的属性的值转化为 T 类型

把一个个工具泛型理解成函数,类型作为入参和返回值即可,通过 cmd + 左键点击具体工具泛型阅读具体实现也可。

image.png

推荐阅读