React’s ts type checking and points to note

React’s ts type checking and points to note

When writing React in TypeScript, we can @types/...use some type checks. This article records the points to note about these checks.

Configuration

First perform compilation configuration. The configuration is very simple, just install @types/reactthe package ( npm i -D @types/react) first, and then configure the item in tsconfig.jsonthe compilation configuration item .compilerOptions"jsx""react"

*Options for jsx items

  • "preserve": The code compilation in mode will retain the jsx format and output a .jsxfile with the suffix;
  • "react": Compilation of code in mode is directly converted into React.createElement and outputs a .jsfile with suffix;
  • "react-native": Compilation of the code in mode will retain the jsx format and output a .jsfile with a suffix.
Mode Input Output Output File Extension
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js
react-native <div /> <div /> .js

Commonly used checks

Type checking for functional components

Use FC (Function Component) type to declare function components

FC defines default props (such as children) and some static properties (such as defaultProps). like

import React, {
    
     FC } from 'react';

interface DemoComponentProps {
    
    
  className?: string;
  style?: React.CSSProperties;
}

const DemoComponent: FC<DemoComponentProps> = props => {
    
    
  return <div>hello react</div>;
};

You can also directly use ordinary functions to declare components, which is more flexible:

interface DemoComponentProps {
    
    
  className?: string;
  style?: React.CSSProperties;
  // 手动声明children
  children?: React.ReactNode;
}

function DemoComponent(props: DemoComponentProps) {
    
    
  return <div>hello react</div>;
}

Similar to it is SFC, but it is no longer recommended to use it. The specific reasons can be seen in the source @types/reactcode analysis below.

Define Props type

ComponentName|PropsProps types are usually named using the format of . like:

import React from 'react'

interface DemoComponentProps {
    
    
  title: string;
  content: string;
}

function DemoComponent (props: DemoComponentProps) {
    
    
  const {
    
    
    title,
    content,
  } = props;
  return (<>
    <p>title: {
    
    title}</p>
    <p>content: {
    
    content}</p>
  </>)
}
Children

We set children by setting React.ReactNode. like

import React from 'react'

type CardProps = {
    
    
  title: string;
  children: React.ReactNode;
};

export function Card({
    
     title, children }: CardProps) {
    
    
  return (
    <section className="cards">
      <h2>{
    
    title}</h2>
      {
    
    children}
    </section>
  );
}
Default props declaration

TypeScript 3.0 has supported defaultProps, which also means that we can easily define default values. like:

import React, {
    
     PropsWithChildren } from 'react';

export interface HelloProps {
    
    
  name: string;
}

/**
 * 直接使用函数参数声明
 * PropsWithChildren只是扩展了children, 完全可以自己声明
 * type PropsWithChildren<P> = P & {
 *    children?: ReactNode;
 * }
 */
const Hello = ({
    
     name }: PropsWithChildren<HelloProps>) => <div>Hello {
    
    name}!</div>;

Hello.defaultProps = {
    
     name: 'Wayne' };

This method is very concise, but the type of defaultProps has no correlation with the props of the component itself, which will make defaultProps unable to obtain type constraints, so if necessary, further explicitly declare the type of defaultProps:

Hello.defaultProps = {
    
     name: 'Wayne' } as Partial<HelloProps>;
Dispatch interface

Dispatch<any>Generic interface, used to define the type of dispatch, often used in dispatch generated by useReducer.

/** 
 * 创建一个异步action的函数,返回一个包含异步action对象
 */
const asyncAction = (dispatch: Dispatch<any>) => {
    
    
    return {
    
    
        asyncAddaction() {
    
    // 一个异步的添加action
            console.log('执行addActions之前: ' + Date.now());
            setTimeout(() => {
    
    
                console.log('执行addActions : ' + Date.now());
                dispatch(addActions());
            }, 1000);
        }
    }
}

Generic function component

Generic function components are commonly used in list-type or container-type components. Direct use of FC cannot meet the needs:

import React from 'react';

export interface ListProps<T> {
    
    
  list: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
}

export function List<T>(props: ListProps<T>) {
    
    
  return (
  <section>
	{
    
    
		props.list.map(props.renderItem)
	}
  </section>
  );
}

function TestList() {
    
    
  return (
    <List
      list={
    
    [1, 2, 3]}
      renderItem={
    
    i => {
    
    
        /* TypeScript推断i为number类型 */
		    return (<p>{
    
    i}</p>)
      }}
    />
  );
}
Subcomponent declaration

Using JSX in the form of Parent.Child can make the parent-child relationship of nodes more intuitive. It is similar to a namespace mechanism and can avoid naming conflicts. This type of form is widely used in ant design. Compared to the naming method ParentChild, Parent.Child is more elegant. like:

import React, {
    
     PropsWithChildren } from 'react';

export interface LayoutProps {
    
    }
export interface LayoutHeaderProps {
    
    } // 采用ParentChildProps形式命名
export interface LayoutFooterProps {
    
    }

export function Layout(props: PropsWithChildren<LayoutProps>) {
    
    
  return <div className="layout">{
    
    props.children}</div>;
}

// 作为父组件的属性
Layout.Header = (props: PropsWithChildren<LayoutHeaderProps>) => {
    
    
  return <div className="header">{
    
    props.children}</div>;
};

Layout.Footer = (props: PropsWithChildren<LayoutFooterProps>) => {
    
    
  return <div className="footer">{
    
    props.children}</div>;
};

function TestLayout () {
    
    
  return (<Layout>
    <Layout.Header>header</Layout.Header>
    <Layout.Footer>footer</Layout.Footer>
  </Layout>)
}
Forwarding Refs

React.forwardRefNew in 16.3, it can be used to forward ref, suitable for HOC and function components. ComponentName|MethodsNaming rules that can be used for its exposed methods

/* MyModal.tsx */
import React, {
    
     useState, useImperativeHandle, FC, useRef, useCallback } from 'react';

export interface MyModalProps {
    
    
  title?: React.ReactNode;
  onOk?: () => void;
  onCancel?: () => void;
}

export interface MyModalMethods {
    
    
  show(): void;
}

export const MyModal = React.forwardRef<MyModalMethods, MyModalProps>((props, ref) => {
    
    
  const [visible, setVisible] = useState();

  // 初始化ref暴露的方法
  useImperativeHandle(ref, () => ({
    
    
    show: () => setVisible(true),
  }));

  return <Modal visible={
    
    visible}>...</Modal>;
});

/* Test.tsx */
const Test: FC<{
    
    }> = props => {
    
    
  // 引用
  const modal = useRef<MyModalMethods | null>(null);
  const confirm = useCallback(() => {
    
    
    if (modal.current) {
    
    
      modal.current.show();
    }
  }, []);

  const handleOk = useCallback(() => {
    
    }, []);

  return (
    <div>
      <button onClick={
    
    confirm}>show</button>
      <MyModal ref={
    
    modal} onOk={
    
    handleOk} />
    </div>
  );
};

Type checking for class components

Inherited Component<P, S={}>or PureComponent<P, S={}>generic class, receiving two parameters:
  • P:Type definition of props
  • S: type definition of state
import React from 'react'

export interface CounterProps {
    
    	// 同样是{ComponentName}Props形式命名
  defaultCount: number; // 可选props, 不需要?修饰
}

/**
 * 组件状态
 */
interface State {
    
    
  count: number;
}

/**
 * 继承React.Component, 并声明Props和State类型
 */
export class Counter extends React.Component<CounterProps, State> {
    
    
  /**
   * 默认参数
   */
  public static defaultProps = {
    
    
    defaultCount: 0,
  };

  /**
   * 初始化State
   */
  public state = {
    
    
    count: this.props.defaultCount,
  };

  /**
   * 声明周期方法
   */
  public componentDidMount() {
    
    }
  
  public componentWillUnmount() {
    
    }

  public componentDidCatch() {
    
    }

  public componentDidUpdate(prevProps: CounterProps, prevState: State) {
    
    }

  /**
   * 渲染函数
   */
  public render() {
    
    
    return (
      <div>
        {
    
    this.state.count}
        <button onClick={
    
    this.increment}>Increment</button>
        <button onClick={
    
    this.decrement}>Decrement</button>
      </div>
    );
  }

  private increment = () => {
    
    
    this.setState(({
    
     count }) => ({
    
     count: count + 1 }));
  };

  private decrement = () => {
    
    
    this.setState(({
    
     count }) => ({
    
     count: count - 1 }));
  };
}
Use static defaultProps to define default props

The props defined in defaultProps do not need to ?be modified by optional operators. The demo is as above.

Subcomponent declaration

Class components can declare subcomponents using static attributes, such as:

import React from 'react'
import Header from './Header'
import Footer from './Footer'

export class Layout extends React.Component<LayoutProps> {
    
    
  public static Header = Header;
  public static Footer = Footer;

  public render() {
    
    
    return <div className="layout">{
    
    this.props.children}</div>;
  }
}
Generic components

Generic components are similar to functional components, such as:

import React from 'react'

export class List<T> extends React.Component<ListProps<T>> {
    
    
  public render() {
    
    }
}

Type definitions for other functional components

Extended attributes of HTML elements

You can use JSX.IntrinsicElementscollections to ensure that you can set all HTML attributes of an element. like:

import React from 'react'

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

function Button({
    
     ...allProps }: ButtonProps) {
    
    
  return <button {
    
    ...allProps} />;
}

Default properties:

import React from 'react'

type ButtonProps =
  Omit<JSX.IntrinsicElements["button"], "type">;

function Button({
    
     ...allProps }: ButtonProps) {
    
    
  return <button type="button" {
    
    ...allProps} />;
}

const z = <Button type="button">Hi</Button>; 
Context

Context provides a mechanism for sharing state across components. Usually we use Name|ContextValuethe naming convention to declare the type of Context.

import React, {
    
     FC, useContext } from 'react';

export interface Theme {
    
    
  primary: string;
  secondary: string;
}

export interface ThemeContextValue {
    
    
  theme: Theme;
  onThemeChange: (theme: Theme) => void;
}

/**
 * 创建Context, 并设置默认值, 以 Name|Context 的格式命名
 */
export const ThemeContext = React.createContext<ThemeContextValue>({
    
    
  theme: {
    
    
    primary: 'red',
    secondary: 'blue',
  },
  onThemeChange: noop,
});

/**
 * Provider, 以{Name}Provider命名
 */
export const ThemeProvider: FC<{
    
     theme: Theme; onThemeChange: (theme: Theme) => void }> = props => {
    
    
  return (
    <ThemeContext.Provider value={
    
    {
    
     theme: props.theme, onThemeChange: props.onThemeChange }}>
      {
    
    props.children}
    </ThemeContext.Provider>
  );
};

/**
 * 暴露hooks, 以use{Name}命名
 */
export function useTheme() {
    
    
  return useContext(ThemeContext);
}

*Type checking of higher-order components

To be honest, it is not recommended to use HOC. Higher-order components are clunky and difficult to understand, easily create nesting hell (wrapper), and are not friendly to Typescript typing. But here’s an example:

import React, {
    
     FC } from 'react';

export interface ThemeProps {
    
    
  primary: string;
  secondary: string;
}

/**
 * 给指定组件注入'主题'
 */
export function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
    
    
  /**
   * WithTheme 自己暴露的Props
   */
  interface OwnProps {
    
    }

  /**
   * 高阶组件的props, 忽略ThemeProps, 外部不需要传递这些属性
   */
  type WithThemeProps = P & OwnProps;

  /**
   * 高阶组件
   */
  const WithTheme = (props: WithThemeProps) => {
    
    
    // 假设theme从context中获取
    const fakeTheme: ThemeProps = {
    
    
      primary: 'red',
      secondary: 'blue',
    };
    return <Component {
    
    ...fakeTheme} {
    
    ...props} />;
  };

  WithTheme.displayName = `withTheme${
      
      Component.displayName}`;

  return WithTheme;
}

// Test
const Foo: FC<{
    
     a: number } & ThemeProps> = props => <div style={
    
    {
    
     color: props.primary }} />;
const FooWithTheme = withTheme(Foo);
() => {
    
    
  <FooWithTheme a={
    
    1} />;
};

or

/**
 * 抽取出通用的高阶组件类型
 */
type HOC<InjectedProps, OwnProps = {
    
    }> = <P>(
  Component: React.ComponentType<P & InjectedProps>,
) => React.ComponentType<P & OwnProps>;

/**
 * 声明注入的Props
 */
export interface ThemeProps {
    
    
  primary: string;
  secondary: string;
}

export const withTheme: HOC<ThemeProps> = Component => props => {
    
    
  // 假设theme从context中获取
  const fakeTheme: ThemeProps = {
    
    
    primary: 'red',
    secondary: 'blue',
  };
  return <Component {
    
    ...fakeTheme} {
    
    ...props} />;
};

*Render Props

React's props (including children) are not type-limited, they can be a function. So there are render props, which are as common a pattern as higher-order components:

import React from 'react';

export interface ThemeConsumerProps {
    
    
  children: (theme: Theme) => React.ReactNode;
}

export const ThemeConsumer = (props: ThemeConsumerProps) => {
    
    
  const fakeTheme = {
    
     primary: 'red', secondary: 'blue' };
  return props.children(fakeTheme);
};

// Test
<ThemeConsumer>
  {
    
    ({
    
     primary }) => {
    
    
    return <div style={
    
    {
    
     color: primary }} />;
  }}
</ThemeConsumer>;

Event type definition

Use handleEvent to name the event handler.

If there are multiple identical event handlers, they are handle|Type|Eventnamed in the format of , for example: handleNameChange.

import React from 'react';

export const EventDemo: FC<{
    
    }> = props => {
    
    
  const handleClick = useCallback<React.MouseEventHandler>(evt => {
    
    
    evt.preventDefault();
    // ...
  }, []);

  return <button onClick={
    
    handleClick} />;
};

Events

Commonly used Event event object types:

  • ClipboardEvent<T = Element>Clipboard event object

  • DragEvent<T = Element>drag event object

  • ChangeEvent<T = Element>Change event object

  • KeyboardEvent<T = Element>keyboard event object

  • MouseEvent<T = Element>mouse event object

  • TouchEvent<T = Element>touch event object

  • WheelEvent<T = Element>wheel event object

  • AnimationEvent<T = Element>animation event object

  • TransitionEvent<T = Element>transition event object

  • FormEvent: The type of a react form event

demos:

<form 
	onSubmit={
    
    (e:FormEvent)=>{
    
    
	    e.preventDefault();//取消默认事件
}}>
<input 
	type="text" 
	value={
    
    count} 
	onChange={
    
    (e: ChangeEvent<HTMLInputElement>) => {
    
    
	   setCount(e.currentTarget.value);// HTMLInputElement表示这个一个html的input节点
	}} />
Types of built-in event handlers

@types/reactThe following event handler types are built-in:

type EventHandler<E extends SyntheticEvent<any>> = {
    
     bivarianceHack(event: E): void }['bivarianceHack'];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;

Event handler types can be declared succinctly:

import {
    
     ChangeEventHandler } from 'react';
export const EventDemo: FC<{
    
    }> = props => {
    
    
  /**
   * 可以限定具体Target的类型
   */
  const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(evt => {
    
    
    console.log(evt.target.value);
  }, []);

  return <input onChange={
    
    handleChange} />;
};

Custom component exposes event handler type

Like native HTML elements, custom components should expose their own event handler types, especially more complex event handlers. This avoids developers manually declaring types for the parameters of each event handler. Custom event handler types
and ComponentName|Event|Handlerformat naming. In order to distinguish it from the native event handler type, the suffix in the form of EventHandler is not used.

import React, {
    
     FC, useState } from 'react';

export interface UploadValue {
    
    
  url: string;
  name: string;
  size: number;
}

/**
 * 暴露事件处理器类型
 */
export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;

export interface UploadProps {
    
    
  value?: UploadValue;
  onChange?: UploadChangeHandler;
}

export const Upload: FC<UploadProps> = props => {
    
    
  return <div>...</div>;
};

Other type checks

Get native element props definition

In some scenarios, we hope that native elements will extend some props. All native element props inherit React.HTMLAttributes, and some special elements will also extend their own attributes, such as InputHTMLAttributes. For details, please refer to the implementation of the React.createElement method.

import React, {
    
     FC } from 'react';

export function fixClass<
  T extends Element = HTMLDivElement,
  Attribute extends React.HTMLAttributes<T> = React.HTMLAttributes<T>
>(cls: string, type: keyof React.ReactHTML = 'div') {
    
    
  const FixedClassName: FC<Attribute> = props => {
    
    
    return React.createElement(type, {
    
     ...props, className: `${
      
      cls} ${
      
      props.className}` });
  };

  return FixedClassName;
}

/**
 * Test
 */
const Container = fixClass('card');
const Header = fixClass('card__header', 'header');
const Body = fixClass('card__body', 'main');
const Footer = fixClass('card__body', 'footer');

const Test = () => {
    
    
  return (
    <Container>
      <Header>header</Header>
      <Body>header</Body>
      <Footer>footer</Footer>
    </Container>
  );
};

styled-components

styled-components is a popular CSS-in-js library. Typescript supports generic tag templates in 2.9, so you can simply type constraints on components created by styled-components.

// 依赖于@types/styled-components
import styled from 'styled-components/macro';

const Title = styled.h1<{
    
     active?: boolean }>`
  color: ${
      
      props => (props.active ? 'red' : 'gray')};
`;

// 扩展已有组件
const NewHeader = styled(Header)<{
    
     customColor: string }>`
  color: ${
      
      props => props.customColor};
`;

Customize module declarations for third-party libraries that do not provide Typescript declaration files
// global.d.ts

// 自定义模块声明
declare module 'awesome-react-component' {
    
    
  // 依赖其他模块的声明文件
  import * as React from 'react';
  export const Foo: React.FC<{
    
     a: number; b: string }>;
}

Type definition of axios
import axios, {
    
     AxiosInstance, AxiosRequestConfig, AxiosResponse,AxiosError } from 'axios'
const server: AxiosInstance = axios.create();
server.interceptors.request.use((config: AxiosRequestConfig) => {
    
    //请求拦截
    return config;
});
server.interceptors.response.use((res: AxiosResponse) => {
    
    
    if (res.status === 200) {
    
    //请求成功后 直接需要的返回数据
        res = res.data;
    }
    return res;
},(err:AxiosError)=>{
    
    });

Preact type checking

First, the tsconfig.json configuration is different from React:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxFactory": "h",	// Preact的虚拟DOM解析
    "jsxFragmentFactory": "Fragment"	// Preact的fragment组件解析
    ...
  }
  ...
}

What's more "pitched" is that Fragment and h need to be introduced in each component to inform the ts parsing mode.

import {
    
     Fragment, h } from 'preact'

@types/reactSource code analysis

@types/reactThe source code actually carefully defines some interfaces involved in React, and the notes on the source code are also relatively complete. Source code address: github @types/react

SFC(Stateless Function Component)和FC

I have been curious about the difference between SFC and FC earlier, and I found out when looking at the source code

type SFC<P = {
    
    }> = FunctionComponent<P>;
type FC<P = {
    
    }> = FunctionComponent<P>;

interface FunctionComponent<P = {
    
    }> {
    
    
    (props: PropsWithChildren<P>, context?: any): ReactElement | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

In the current @types/reactdefinition, SFC and FC point to FunctionComponentthis interface, which means they are the same.
The main reason can be seen in DefinitelyTyped pull-30364 . Simply put, SFC is deprecated, but it is still retained to ensure compatibility with old businesses and improve semantics. The overall result is that React.SFC, React.StatelessComponent, React.FC, and React.FunctionComponent are all the same interface

*Former SFC
type SFC<P = {
    
    }> = StatelessComponent<P>;
interface StatelessComponent<P = {
    
    }> {
    
    
    (props: P & {
    
     children?: ReactNode }, context?: any): ReactElement<any> | null;
    propTypes?: ValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

It is found that the difference is actually the difference ValidationMapbetween the sum of propTypes items ( the definition of ValidationMap is visible ), that is, the props verification of the old SFC is more strict:WeakValidationMaptype PropsWithChildren<P> = P & { children?: ReactNode };@types/prop-types

// ValidationMap
export type ValidationMap<T> = {
    
     [K in keyof T]?: Validator<T[K]> };

// WeakValidationMap
type WeakValidationMap<T> = {
    
    
    [K in keyof T]?: null extends T[K]
        ? Validator<T[K] | null | undefined>
        : undefined extends T[K]
        ? Validator<T[K] | null | undefined>
        : Validator<T[K]>
};

// Validator
export interface Validator<T> {
    
    
    (props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
    [nominalTypeHack]?: T;
}

Events

@types/reactIt took a lot of space to encapsulate the event interface. After all, DOM is one of the most complex modules in the front-end. Such as the encapsulation of touch events:

interface TouchEvent<T = Element> extends SyntheticEvent<T, NativeTouchEvent> {
    
    
    altKey: boolean;
    changedTouches: TouchList;
    ctrlKey: boolean;
    /**
     * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
     */
    getModifierState(key: string): boolean;
    metaKey: boolean;
    shiftKey: boolean;
    targetTouches: TouchList;
    touches: TouchList;
}

@types/reactContent interface in

    interface Context<T> {
    
    
        Provider: Provider<T>;
        Consumer: Consumer<T>;
        displayName?: string;
    }
	

It can be found that we need to pass a type so that the parameter types inside are consistent.

other

@types/reactThere are many ingenious designs, which can be read through "@types react noteworthy TS skills" .


important point

1. Do not directly use export default to export anonymous function components

like

export default (props: {
    
    }) => {
    
    
  return <div>hello react</div>;
};

Components exported in this way will appear as Unknown when viewed in React Inspector. Can be modified to:

export default Hello (props: {
    
    }) => {
    
    
  return <div>hello react</div>;
};

2. Give up PropTypes

With Typescript, Props and State can be safely constrained. There is no need to introduce React.PropTypes, and its expressive ability is relatively weak.

3. Controversy over whether to use FC

There has been controversy about whether to use FC, such as "typescript-react-why-i-dont-use-react-fc" , which gives 5 reasons. Generally speaking, it is more flexible and scalable without FC.

From my personal point of view, FC will make the code more semantic. It is recommended to use it if it can be ensured that the project does not migrate React-like technology stacks (preact, taro, rax, etc.).


Related Links

Guess you like

Origin blog.csdn.net/qq_24357165/article/details/108791571