前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React+TypeScript使用规范

React+TypeScript使用规范

原创
作者头像
用户4619307
发布2023-05-04 17:40:03
4.6K0
发布2023-05-04 17:40:03
举报
文章被收录于专栏:TangPieceTangPiece

参考文档: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>
);

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不使用React.FC
  • 基本数据类型不需要显示声明
  • useState
  • useRef
  • createRef
  • useReducer
  • Context
    • 直接使用createContext和useContext
      • 封装createCtx函数
        • 要类型不要默认值
        • 要默认值不要类型
    • forwardRef
    • useImperativeHandle
    • 联合类型
      • in 操作符收窄
        • 类型判断式(type predicates)
          • 可辨别联合(Discriminated unions)
            • 穷尽检查(Exhaustiveness checking)
              • Typeof
              • 枚举
              • 类组件
              • 事件
                • 内联形式
                  • 事件函数:
                    • 使用FormEvent<T>
                    • 使用React.ChangeEventHandler<T>
                    • 不关心事件类型:React.SyntheticEvent
                • 回调函数中的可选参数
                • 获取组件的Props:React.ComponentType
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档