React+TypeScript使用规范

2023-05-04 17:40:03 浏览数 (2)

参考文档:React TypeScript Cheatsheet

不使用React.FC

代码语言:javascript复制
// Bad
const ViewDemo: React.FC<ViewDemoProps> = (props) => {
  // ...
  
  return (
    <div>
      这是使用React.FC类型声明的
    </div>
  )
}


// Good
const ViewDemo = (props: ViewDemoProps) => {
  // ...
  
  return (
    <div>
      这是不使用React.FC类型声明的
    </div>
  )
}

基本数据类型不需要显示声明

提供初始值后,booleanstringnumber类型可以通过类型推断得出

代码语言:javascript复制
// Good
const loading = true

// Good
const CookieKey = 'cookie-key'

// Good
const maxCount = 10

useState

代码语言:javascript复制
// Bad
const [state, setState] = useState<boolean>(false)
const [user, setUser] = useState<User>(null)

// Good
const [state, setState] = useState(false)
const [user, setUser] = useState<User | null>(null)

useRef

引用Dom元素

代码语言:javascript复制
// Bad
const divRef = useRef(null)
// etc...
<div ref={divRef}>etc</div>

// Good
const divRef = useRef<HTMLDivElement>(null);
// etc...
<div ref={divRef}>etc</div>

引用可变值,如定时器

代码语言:javascript复制
// Bad
const intervalRef = useRef();
intervalRef.current = setInterval(() => {
  console.log('setInterval')
}, 1000)

// Good
const intervalRef = useRef<number>();
useEffect(() => {
  // 自己管理current的值
  intervalRef.current = setInterval(() => {
    console.log('setInterval')
  }, 1000)
  return () => clearInterval(intervalRef.current);
}, []);

createRef

代码语言:javascript复制
// Bad
const divRef = createRef(null)
// etc...
<div ref={divRef}>etc</div>

// Good
const divRef = createRef<HTMLDivElement>(null);
// etc...
<div ref={divRef}>etc</div>

useReducer

Bad:没有声明state、action的类型

代码语言:javascript复制
import React, { useReducer } from 'react'

const initialState = {count: 0}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count   action.payload};
    case 'decrement':
      return {count: state.count - Number(action.payload)};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement', payload: '5'})}>-</button>
      <button onClick={() => dispatch({type: 'increment', payload: 5})}> </button>
    </>
  );
}

Good:

代码语言:javascript复制
import React, { useReducer } from 'react';

const initialState = {count: 0};

// 声明为可辨别联合类型
type ACTIONTYPE =
  | { type: 'increment', payload: number}
  | { type: 'decrement', payload: string}

// 使用typeof获取变量initialState的类型,如果数据较多,显示声明state类型
function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case 'increment':
      return {count: state.count   action.payload};
    case 'decrement':
      return {count: state.count - Number(action.payload)};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement', payload: '5'})}>-</button>
      <button onClick={() => dispatch({type: 'increment', payload: 5})}> </button>
    </>
  );
}

Context

Bad:createContext没有声明类型参数

代码语言:javascript复制
import React, { createContext } from "react";

interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}

const AppCtx = createContext();

// Provider in your app
const sampleAppContext: AppContextInterface = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

export const App = () => (
  <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);

// ---组件里使用---
import { useContext } from "react";

export const PostInfo = () => {
  const appContext = useContext(AppCtx);
  return (
    <div>
      Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
      {appContext?.url}
    </div>
  );
};

Good

直接使用createContextuseContext

代码语言:javascript复制
import React, { createContext } from "react";

interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}

// 设置了类型AppContextInterface,但是没有提供默认值
const AppCtx = createContext<AppContextInterface | null>(null);

// Provider in your app
const sampleAppContext: AppContextInterface = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

export const App = () => (
  <AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);

// ---组件里使用---
import { useContext } from "react";

export const PostInfo = () => {
  const appContext = useContext(AppCtx);
  return (
    <div>
      Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
      {appContext?.url}
    </div>
  );
};

封装createCtx函数

要类型不要默认值
代码语言:javascript复制
// create-ctx.ts

import React, { createContext, useContext } from "react";

// context只指定类型,不提供默认值
export function createCtx<A extends {} | null>() {
  // 将默认值设置为了undefined
  const ctx = createContext<A | undefined>(undefined);
  function useCtx() {
    const c = useContext(ctx);
    if (c === undefined)
      throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }
  return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
}

// ---------------使用Provider---------------------
// Provider.tsx

// import React from 'react'
// import { createCtx } from '*/create-ctx'

interface AppContextInterface {
  name: string;
  author: string;
  url: string;
}

export const [useCtx, ContextProvider] = createCtx<AppContextInterface>()

const sampleAppContext: AppContextInterface = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

export const App = () => (
  <ContextProvider value={sampleAppContext}>...</ContextProvider>
);

// -----------------组件中使用-------------------
// import React from 'react'
// import { useCtx } from '*/Provide'

export const PostInfo = () => {
  const appContext = useCtx();
  return (
    <div>
      Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
      {appContext?.url}
    </div>
  );
};

在Typescript Playground中查看

要默认值不要类型
代码语言:javascript复制
// create-ctx.ts

import React, { createContext, useContext, useState } from "react";

// context无指定类型,有默认值
export function createCtx<A>(defaultValue: A) {
  type UpdateType = React.Dispatch<React.SetStateAction<typeof defaultValue>>;
  const defaultUpdate: UpdateType = () => defaultValue;
  // 自动添加了一个更新函数update
  const ctx = createContext({
    state: defaultValue,
    update: defaultUpdate,
  });

  function Provider(props: React.PropsWithChildren<{}>) {
    // update函数提供给外部,来更新state
    const [state, update] = useState(defaultValue);
    const val = useMemo(() => {
      return {
        state,
        update
      }
    }, [state])
    return <ctx.Provider value={val} {...props} />
  }

  function useCtx() {
    const c = useContext(ctx);
    if (c === undefined)
      throw new Error("useCtx must be inside a Provider with a value");
    return c;
  }

  return [useCtx, Provider] as const; // alternatively, [typeof ctx, typeof Provider]
}

// ---------------使用Provider---------------------
// Provider.tsx

// import React from 'react'
// import { createCtx } from '*/create-ctx'

const sampleAppContext = {
  name: "Using React Context in a Typescript App",
  author: "thehappybug",
  url: "http://www.example.com",
};

// 虽然设置了类型AppContextInterface,但是没有提供默认值
export const [useCtx, ContextProvider] = createCtx(sampleAppContext)

export const App = () => (
  <ContextProvider>...</ContextProvider> // <-- 这里不用再注入value
);

// ---------------组件中使用---------------------
// import React from 'react'
// import { useCtx } from '*/Provide'

export const PostInfo = () => {
  const { state, update } = useCtx(); // <-- 多了update函数
  return (
    <div>
      Name: {state?.name}, Author: {state?.author}, Url:{" "}
      {state?.url}
    </div>
  );
};

在TypeScript Playground中查看

forwardRef

Bad:没有声明forwardRef泛型的类型参数

代码语言:javascript复制
import React, { forwardRef, ReactNode, useRef } from "react"

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

// ----------使用------------
const App = () => {
  const btnRef = useRef(null)

  return (
    <FancyButton type="button" ref={btnRef} />
  )
}

Good

代码语言:javascript复制
import React, { forwardRef, ReactNode, useRef } from "react";

interface Props {
  children?: ReactNode;
  type: "submit" | "button";
}

// 提供给使用FancyButton的地方使用
export type Ref = HTMLButtonElement;

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

// ----------使用------------
// import { Ref } from '*/FancyButton'

const App = () => {
  const btnRef = useRef<Ref | null>(null)

  return (
    <FancyButton type="button" ref={btnRef} />
  )
}

在TypeScript Playground中查看

如果不想要外部使用时再手动指定Ref类型,可以修改为

代码语言:javascript复制
import React, { forwardRef, ReactNode, Ref, useRef } from "react";

interface Props {
  children?: ReactNode;
  type: "submit" | "button";
}

export const FancyButton = forwardRef( // <-- 没有再指定泛型类型参数
  (
    props: Props,
    ref: Ref<HTMLButtonElement> // <-- 限定参数类型
  ) => (
    <button ref={ref} className="MyClassName" type={props.type}>
      {props.children}
    </button>
  )
);

// ----------------------
// 使用
// import { Ref } from '*/FancyButton'

const App = () => {
  const btnRef = useRef(null) // <-- 这里不用再指定类型

  return (
    <FancyButton type="button" ref={btnRef} />
  )
}

useImperativeHandle

Bad:没有声明useImperativeHandle的类型

代码语言:javascript复制
// Countdown.tsx
import React, { useImperativeHandle, forwardRef } from 'react'

const Countdown = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    start() {
      alert("Start")
    }
  }))

  return <div>Countdown</div>
})

Good:

代码语言:javascript复制
// Countdown.tsx
import React, { useImperativeHandle, forwardRef } from 'react'

// 定义传给forwardRef的类型
export interface CountdownHandle {
  start: () => void
}

// 组件本身的属性类型
interface CountdownProps {
  time: number
}

const Countdown = forwardRef<CountdownHandle, CountdownProps>((props, ref) => {
  useImperativeHandle(ref, () => ({
    // start() has type inference here
    start() {
      alert("Start")
    }
  }))

  return <div>Countdown</div>
})

使用Countdown组件

代码语言:javascript复制
import React, { useRef, useEffect } from 'react'

import Countdown, { CountdownHandle } from "./Countdown.tsx";

function App() {
  const countdownEl = useRef<CountdownHandle>(null);

  useEffect(() => {
    if (countdownEl.current) {
      // start() has type inference here as well
      countdownEl.current.start();
    }
  }, []);

  return <Countdown ref={countdownEl} />;
}

自定义Hook

Bad:实际返回的类型非期望的类型

代码语言:javascript复制
import React from 'react'

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  return [isLoading, load];
}

Good:

代码语言:javascript复制
import React from 'react'

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) => {
    setState(true);
    return aPromise.finally(() => setState(false));
  };
  // 使用as const将返回值限定为只读元组
  return [isLoading, load] as const;
}

联合类型

使用联合类型时需要进行类型收窄

in 操作符收窄

in 操作符可以判断一个对象是否有对应的属性名,可以通过这个收窄对象类型

代码语言:javascript复制
type LinkProps = Omit<JSX.IntrinsicElements["a"], "href"> & { to?: string };

function RouterLink(props: LinkProps | AnchorProps) {
  // Good
  if ("href" in props) {
    return <a {...props} />;
  } else {
    return <Link {...props} />;
  }
}

类型判断式(type predicates)

一个采用 parameterName is Type的形式返回 boolean 值的函数,但 parameterName 必须是当前函数的参数名

代码语言:javascript复制
// Button props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  href?: undefined;
};

// Anchor props
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
  href?: string;
};

// Input/output options
type Overload = {
  (props: ButtonProps): JSX.Element;
  (props: AnchorProps): JSX.Element;
};

// 通过in判断有无href,用is断言是否是AnchorProps类型
const hasHref = (props: ButtonProps | AnchorProps): props is AnchorProps =>
  "href" in props;

// Component
const Button: Overload = (props: ButtonProps | AnchorProps) => {
  // hasHref的返回值中用is断言了类型,所以这里可以推断出具体类型
  if (hasHref(props)) return <a {...props} />;
  // button render
  return <button {...props} />;
};

在TypeScript Playground中查看

ButtonProps、AnchorProps也可以使用JSX.IntrinsicElements声明

代码语言:javascript复制
import React from 'react'

type ButtonProps = JSX.IntrinsicElements["button"];
type AnchorProps = JSX.IntrinsicElements["a"];

// Input/output options
type Overload = {
  (props: ButtonProps): JSX.Element;
  (props: AnchorProps): JSX.Element;
};

// 通过in判断有无href,用is断言是否是AnchorProps类型
const hasHref = (props: ButtonProps | AnchorProps): props is AnchorProps =>
  "href" in props;

// Component
const Button: Overload = (props: ButtonProps | AnchorProps) => {
  // hasHref的返回值中用is断言了类型,所以这里可以推断出具体类型
  if (hasHref(props)) return <a {...props} />;
  // button render
  return <button {...props} />;
};

在TypeScript Playground中查看

可辨别联合(Discriminated unions)

Bad:

代码语言:javascript复制
// Bad

interface Shape {
  kind: 'circle' | 'square';
  radius?: number;
  sideLength?: number;
}

function getArea(shape: Shape) {
  if (shape.kind === 'circle') {
    return Math.PI * shape.radius ** 2;
    // Object is possibly 'undefined'.
  }
  if (shape.kind === 'square') {
    return shape.sideLength ** 2
    // Object is possibly 'undefined'
  }
  return 0
}

Good:

代码语言:javascript复制
interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
  if (shape.kind === 'square') {
    return shape.sideLength ** 2
  }
  return 0
}

穷尽检查(Exhaustiveness checking)

利用任何类型都不能赋值给 never 类型(除了 never 自身)的特性,实现穷尽检查。

我们可以将上例中最后的return 0进行优化

代码语言:javascript复制
interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
  if (shape.kind === 'square') {
    return shape.sideLength * shape.sideLength
  }
  const _exhaustiveCheck: never = shape;
  return _exhaustiveCheck;
}

这样,当我们给Shape增加其他类型时,就会有ts报错,提醒我们必须处理新加的类型

代码语言:javascript复制
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

type Shape = Circle | Square | Rectangle;

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  }
  if (shape.kind === 'square') {
    return shape.sideLength * shape.sideLength
  }
  // error
  const _exhaustiveCheck: never = shape;
  // Type 'Rectangle' is not assignable to type 'never'.
  return _exhaustiveCheck;
}

Typeof

JavaScript 本身就提供了 typeof 操作符,可以返回运行时一个值的基本类型信息:

  • "string"
  • "number"
  • "bigInt"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

可以看出,typeof可以用来收窄基础类型stringnumberbooleansymbol

代码语言:javascript复制
function padLeft(padding: number | string, input: string) {
  // ok
  console.log(padding.valueOf())

  // Good
  if (typeof padding === "number") {
    return new Array(padding   1).join(" ")   input;
  }
  return padding   input;
}

但不能收窄null或具体的对象类型

代码语言:javascript复制
function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
      // Object is possibly 'null'.
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } else {
    // do nothing
  }
}

枚举

可以用联合类型代替的

代码语言:javascript复制
// Bad
export enum Postion {
  left = 'left',
  right = 'right',
  top = 'top',
  bottom = 'bottom'
}

// Good
export type Position = "left" | "right" | "top" | "bottom";

类组件

Bad:未指定组件propsstate的类型

代码语言:javascript复制
import React from 'react'

class App extends React.Component {
  state = {
    count: 0
  }
  
  render() {
    return (
      <div>
        {this.props.message} {this.state.count}
      </div>
    );
  }
}

Good:

代码语言:javascript复制
import React from 'react'

// 声明class组件的props类型,也可以使用 `interface`
interface MyProps {
  message: string;
};

// 声明class组件的state类型
interface MyState {
  count: number;
};

// 指定App的props类型为MyProps,state类型为MyState
class App extends React.Component<MyProps, MyState> {
  // 这里再次声明state类型为MyState,是为了方便初始化(有代码提示)
  state: MyState = {
    count: 0
  }
  
  render() {
    return (
      <div>
        {this.props.message} {this.state.count}
      </div>
    );
  }
}

事件

内联形式

Good:内联形式event的类型会通过类型推断得到

代码语言:javascript复制
import React from 'react'

const el = (
  <button
    onClick={(event) => {
      console.log('event的类型会通过类型推断得到')
    }}
  />
);

事件函数:

Bad:没有声明e的类型,handleChange没有使用useCallback包裹

代码语言:javascript复制
import React from 'react'

const App = () => {
  const handleChange = (e) => {
    console.log('value=', e.currentTarget.value)
  }
  
  return (
    <div>
      <input type="text" handleChange={handleChange} />
    </div>
  )
}

Good:

使用FormEvent<T>
代码语言:javascript复制
import React, { useCallback } from 'react'

const App = () => {
  // 根据 = 号右边的函数类型推断出handleChange的类型
  const handleChange = useCallback((e: React.FormEvent<HTMLInputElement>): void => {
    console.log('value=', e.currentTarget.value)
  }, [])
  
  return (
    <div>
      <input type="text" handleChange={handleChange} />
    </div>
  )
}
使用React.ChangeEventHandler<T>
代码语言:javascript复制
import React, { useCallback } from 'react'

const App = () => {
  // 类型声明在 = 号左侧
  const handleChange: React.ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
    console.log('value=', e.currentTarget.value)
  }, [])
  
  return (
    <div>
      <input type="text" handleChange={handleChange} />
    </div>
  )
}
不关心事件类型:React.SyntheticEvent
代码语言:javascript复制
import React, { useCallback, useRef } from 'react'

const Form = () => {
  const formRef = useRef<HTMLFormElement>(null)

  const handleSubmit = useCallback((e: React.SyntheticEvent) => {
    e.preventDefault();
    // typeof获取e.target的类型
    // & 通过交叉给e.target类型扩展自定义的字段
    // as 将e.target断言为指定类型
    // 这样,e.target就可以访问email、password属性
    const target = e.target as typeof e.target & {
      email: { value: string };
      password: { value: string };
    };
    const email = target.email.value; // typechecks!
    const password = target.password.value; // typechecks!
    // etc...
  }, [])
  
  return (
    <form
      ref={formRef}
      onSubmit={handleSubmit}
    >
      <div>
        <label>
          Email:
          <input type="email" name="email" />
        </label>
      </div>
      <div>
        <label>
          Password:
          <input type="password" name="password" />
        </label>
      </div>
      <div>
        <input type="submit" value="Log in" />
      </div>
    </form>
  )
}

参考:事件类型对照表

回调函数中的可选参数

回调函数中不应该使用可选参数。也就是说,调用callback时,要提供所有所需参数,是否使用这些参数应该由使用者自己决定

代码语言:javascript复制
// Bad
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i  ) {
    callback(arr[i], i);
  }
}


// Good
function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
  for (let i = 0; i < arr.length; i  ) {
    callback(arr[i], i);
  }
}

获取组件的Props:React.ComponentType

封装工具类:$ElementProps<T>

代码语言:javascript复制
// react-utility-types.d.ts

import React from 'react'

export type $ElementProps<T> = T extends React.ComponentType<infer Props>
  ? Props extends object
    ? Props
    : never
  : never;

使用

代码语言:javascript复制
import React from "react";
import { Modal } from 'antd'
import { $ElementProps } from "*/react-utility-types";

const CustomModal = (
  { title, children, ...props }: $ElementProps<typeof Modal> // new utility, see below
) => (
  <div {...props}>
    {title}: {children}
  </div>
);

0 人点赞