Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
worldzhao opened this issue Sep 8, 2022 · 0 comments
Open

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

worldzhao opened this issue Sep 8, 2022 · 0 comments

Comments

@worldzhao
Copy link
Owner

worldzhao commented Sep 8, 2022

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 定义移除了内置 children 声明
  2. 泛型组件属于低频场景
  3. 作为命名空间挂载静态属性可以别扭实现,React.FC & {key: value}
  4. defaultProps 早已被解构默认值替代

所以是否使用 React.FC 可以自行选择:

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

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>
  );
};

// 作为命名空间
Hello.World = () => <div>hello, world</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>;

另外一个例子:

typescript-2

类型收窄

某些场景传入的参数为联合类型,需要基于一些手段将其类型收窄(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 + 左键点击具体工具泛型阅读具体实现也可。

typescript-1

推荐阅读

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant