It’s been more than 3 months since I graduated, and I’m working on a stack of React hooks + TypeScript. There aren’t too many pitfalls with TypeScript alone, but it gets a lot more complicated with React. This article takes a look at some of the common type definition problems encountered when working with TypeScript and React. Before reading this article, you should have some basic knowledge of React and TypeScript. The article content is more, the suggestion collects again to learn first!

Component declaration

React components are declared in two ways: function components and class components. Let’s see how these two types of components are declared.

1. The type of component

Class components can be defined in two ways: PureComponent

and react. PureComponent

. These are generic interfaces that accept two parameters: props and state. Can be omitted if there is no:

,>
,>

interface IProps {
  name: string; }! [three even. GIF] (HTTPS://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b92bebd2b1042b9b0bc6303dc16ea20~tplv-k3u1fbpfcp-watermark.image?)
interface IState {
  count: number;
}

class App extends React.Component<IProps.IState> {
  state = {
    count: 0
  };

  render() {
    return (
      <div>
        {this.state.count}
        {this.props.name}
      </div>); }}export default App;
Copy the code

React.PureComponent

,>

class App extends React.PureComponent<IProps.IState> {}
Copy the code

React.PureComponent takes a third parameter, which represents the return value of getSnapshotBeforeUpdate.

What’s the difference between a PureComponent and a Component? The main difference is that shouldComponentUpdate in PureComponent is handled by itself, not by us, so PureComponent can improve performance to some extent.

Sometimes you might see this notation, but it actually does the same thing:

import React, {PureComponent, Component} from "react";

class App extends PureComponent<IProps.IState> {}

class App extends Component<IProps.IState> {}
Copy the code

What if we don’t know the props type of the component at definition time, but only know the component type when we call it? This is where generics come into play:

// Define the component
class MyComponent<P> extends React.Component<P> {
  internalProp: P;
  constructor(props: P) {
    super(props);
    this.internalProp = props;
  }
  render() {
    return (
    	 <span>hello world</span>); }}// Use the component
type IProps = { name: string; age: number; };

<MyComponent<IProps> name="React" age={18} / >;// Success
<MyComponent<IProps> name="TypeScript" age="hello"/ >;// Error
Copy the code

2. Function components

Normally, I would write a function component like this:

interface IProps {
  name: string
}

const App = (props: IProps) = > {
  const {name} = props;

  return (
    <div className="App">
      <h1>hello world</h1>
      <h2>{name}</h2>
    </div>
  );
}

export default App;
Copy the code

Function types can also be defined using react. FunctionComponent

, or simply react. FC

, which has the same effect. It is a generic interface that accepts a parameter that is not required to represent the type of props. They are equivalent to this:

={}>
={}>

type React.FC<P = {}> = React.FunctionComponent<P>
Copy the code

The final definition is as follows:

interface IProps {
  name: string
}

const App: React.FC<IProps> = (props) = > {
  const {name} = props;
  return (
    <div className="App">
      <h1>hello world</h1>
      <h2>{name}</h2>
    </div>
  );
}

export default App;
Copy the code

When defining a function component in this form, the props element has the children attribute, which represents the internal elements of the component when it is called. Here is an example. First we define a component in which Child1 and Child2 components are introduced:

import Child1 from "./child1";
import Child2 from "./child2";

interface IProps {
  name: string;
}
const App: React.FC<IProps> = (props) = > {
  const { name } = props;
  return (
    <Child1 name={name}>
      <Child2 name={name} />
      TypeScript
    </Child1>
  );
};

export default App;
Copy the code

The structure of Child1 components is as follows:

interface IProps {
  name: string;
}
const Child1: React.FC<IProps> = (props) = > {
  const { name, children } = props;
  console.log(children);
  return (
    <div className="App">
      <h1>hello child1</h1>
      <h2>{name}</h2>
    </div>
  );
};

export default Child1;
Copy the code

We print the children property in the Child1 component, and its value is an array containing the Child2 object and the following text:

The difference between declaring function components using React.FC and normal declarations is as follows:

  • React.FC explicitly defines return types, while other methods are derived implicitly;
  • React.FC provides type checking and auto-completion for static properties: displayName, propTypes, and defaultProps;
  • The React. FC provides children with implicit type (ReactElement | null).

If we don’t know the type of props when we define the component and only know it when we call it, we still use generics to define the type of props. For function components defined using function:

// Define the component
function MyComponent<P> (props: P) {
  return (
  	<span>
    	{props}
    </span>
  );
}

// Use the component
type IProps = { name: string; age: number; };

<MyComponent<IProps> name="React" age={18} / >;// Success
<MyComponent<IProps> name="TypeScript" age="hello"/ >;// Error
Copy the code

If you use a function component defined by the arrow function, it would be an error to call it directly:

const MyComponent = <P>(props: P) {
  return (
  	<span>
    	{props}
    </span>
  );
}
Copy the code

The extends keyword must be used to define generic parameters to be successfully parsed:

const MyComponent = <P extends any>(props: P) {
  return (
  	<span>
    	{props}
    </span>
  );
}
Copy the code

React built-in types

1. JSX.Element

Let’s start with the jsx. Element type declaration:

declare global {
  namespace JSX {
    interface Element extends React.ReactElement<any, any> { }
  }
}
Copy the code

As you can see, jsx. Element is a subtype of ReactElement. It does not add attributes and the two are equivalent. That is, two types of variables can be assigned to each other.

Jsx. Element can be obtained by executing React. CreateElement or by translating JSX:

const jsx = <div>hello</div>
const ele = React.createElement("div".null."hello");
Copy the code

2. React.ReactElement

ReactElement < T > allows us to annotate instantiations of class components by passing in < T/ >, as defined in the React type declaration file:

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
   type: T;
   props: P;
   key: Key | null;
}
Copy the code

ReactElement is an interface that contains the properties of Type,props, and key. The value of this type can be null or ReactElement instance.

Normally, function components return the value of ReactElement (jxS.Element).

3. React.ReactNode

The ReactNode type is declared as follows:

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
Copy the code

As you can see, ReactNode is a union type. It can be string, Number, ReactElement, NULL, Boolean, or ReactNodeArray. It follows that. Variables of type ReactElement can be assigned directly to variables of type ReactNode, but not vice versa.

The render member function of the class component returns a value of type ReactNode:

class MyComponent extends React.Component {
		render() {
    		return <div>hello world</div>}}/ / right
const component: React.ReactNode<MyComponent> = <MyComponent />;
/ / error
const component: React.ReactNode<MyComponent> = <OtherComponent />;
Copy the code

In the code above, we set the Component variable to a React instance of type Mycomponent. We can only assign it an instance component of Mycomponent.

Normally, the class component returns the value of a ReactNode through Render ().

4. CSSProperties

The React declaration defines CSSProperties:

export interface CSSProperties extends CSS.Properties<string | number> {
  /** * The index signature was removed to enable closed typing for style * using CSSType. You're able to use type assertion or module augmentation * to add properties or an index signature of your own. * * For examples and more information, visit: * https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors */
}
Copy the code

React.CSSProperties is a React typescript-based CSS property type that can be set to the return value of a method:

import * as React from "react";

const classNames = require("./sidebar.css");

interface Props {
  isVisible: boolean;
}

const divStyle = (props: Props): React.CSSProperties => ({
  width: props.isVisible ? "23rem" : "0rem"
});

export const SidebarComponent: React.StatelessComponent<Props> = props= > (
  <div id="mySidenav" className={classNames.sidenav} style={divStyle(props)}>
    {props.children}
  </div>
);
Copy the code

Here the return value of the divStyle component is of type react. CSSProperties.

We can also define a variable of type CSSProperties:

const divStyle: React.CSSProperties = {
    width: "11rem".height: "7rem".backgroundColor: `rgb(${props.color.red}.${props.color.green}.${props.color.blue}) `
};
Copy the code

This variable can be used on the style attribute of an HTML tag:

<div style={divStyle} />
Copy the code

In the React type declaration file, the style property has the following type:

style? : CSSProperties |undefined;
Copy the code

Third, the React of Hooks

1. useState

By default, React automatically deduces the state and update function type based on the initial value of the state set:

If the type of state is known, you can customize the type of state in the following form:

const [count, setCount] = useState<number> (1)
Copy the code

If the initial value is null, we need to explicitly declare the type of state:

const [count, setCount] = useState<number | null> (null); 
Copy the code

If state is an object and you want to initialize an empty object, you can handle this with an assertion:

const [user, setUser] = React.useState<IUser>({} as IUser);
Copy the code

In fact, asserting the empty {} object as the IUser interface is a trick to the TypeScript compiler. Since future code may rely on this object, you should initialize the value of user before using it, or else an error will be reported.

Here is the definition of useState in the declaration file:

function useState<S> (initialState: S | (() => S)) :S.Dispatch<SetStateAction<S> >];
// convenience overload when first argument is omitted
	/**
	 * Returns a stateful value, and a function to update it.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#usestate
   */
    
function useState<S = undefined> () :S | undefined.Dispatch<SetStateAction<S | undefined> >];
  /**
   * An alternative to `useState`.
   *
   * `useReducer` is usually preferable to `useState` when you have complex state logic that involves
   * multiple sub-values. It also lets you optimize performance for components that trigger deep
   * updates because you can pass `dispatch` down instead of callbacks.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#usereducer
   */
Copy the code

As you can see, there are two forms defined here, one with an initial value and the other with no initial value.

2. useEffect

UseEffect is used to handle side effects. Its first argument is a function, indicating the operation to clear the side effect, and its second argument is a set of values. When the set of values changes, the function of the first argument is executed.

useEffect(
  () = > {
    const subscription = props.source.subscribe();
    return () = > {
      subscription.unsubscribe();
    };
  },
  [props.source]
);
Copy the code

If the function returns a value that is not undefined in the function or effect function, it looks like this:

useEffect(
    () = > {
      subscribe();
      return null; });Copy the code

TypeScript reports an error:

Let’s look at useEffect as defined in the type declaration file:

// Destructors are only allowed to return void.
type Destructor = () = > void | { [UNDEFINED_VOID_ONLY]: never };

// NOTE: callbacks are _only_ allowed to return either void, or a destructor.
type EffectCallback = () = > (void | Destructor);

TODO (TypeScript 3.0): ReadonlyArray
      
type DependencyList = ReadonlyArray<any>;

function useEffect(effect: EffectCallback, deps? : DependencyList) :void;
// NOTE: this does not accept strings, but this will have to be fixed by removing strings from type Ref<T>
  /**
   * `useImperativeHandle` customizes the instance value that is exposed to parent components when using
   * `ref`. As always, imperative code using refs should be avoided in most cases.
   *
   * `useImperativeHandle` should be used with `React.forwardRef`.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle
   */
Copy the code

As you can see, the first argument to useEffect allows only one function to be returned.

3. useRef

When using useRef, we can access a mutable reference object. You can pass an initial value to useRef, which initializes the current property exposed by the mutable REF object. When we use useRef, we need to give it a type:

const nameInput = React.useRef<HTMLInputElement>(null)
Copy the code

The instance type is specified as input field type.

When the initial value of useRef is null, there are two forms of creation.

const nameInput = React.useRef<HTMLInputElement>(null)
nameInput.current.innerText = "hello world";
Copy the code

In this form, ref1.current is read-only, so we get the following error when we reassign its innerText property:

Cannot assign to 'current' because it is a read-only property.
Copy the code

To change the current property to dynamically variable, let’s look at how useRef defines it in the type declaration file:

 function useRef<T> (initialValue: T) :MutableRefObject<T>;
 // convenience overload for refs given as a ref prop as they typically start with a null value
 /**
   * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
   * (`initialValue`). The returned object will persist for the full lifetime of the component.
   *
   * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
   * value around similar to how you’d use instance fields in classes.
   *
   * Usage note: if you need the result of useRef to be directly mutable, include `| null` in the type
   * of the generic argument.
   *
   * @version 16.8.0
   * @see https://reactjs.org/docs/hooks-reference.html#useref
   */
Copy the code

The first ten lines of this code tells us that if need useRef variable directly, you need to ‘| null’ contained in the generic parameter, so that’s when the second definition of the initial value is null form:

const nameInput = React.useRef<HTMLInputElement | null> (null);
Copy the code

In this form, nameInput.current is writable. However, both types require type checking when used:

nameInput.current? .innerText ="hello world";
Copy the code

So the question is, why does the first version not report an error when there is no current operation? Since useRef has multiple overload declarations in the type definition, the first method is to perform the following function overload:

function useRef<T> (initialValue: T|null) :RefObject<T>;
// convenience overload for potentially undefined initialValue / call with 0 arguments
// has a default to stop it from defaulting to {} instead
/**
  * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
  * (`initialValue`). The returned object will persist for the full lifetime of the component.
  *
  * Note that `useRef()` is useful for more than the `ref` attribute. It’s handy for keeping any mutable
  * value around similar to how you’d use instance fields in classes.
  *
  * @version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#useref
  */
Copy the code

Function useRef returns a MutableRefObject typed value, where T is the type T of the parameter, so the final nameInput type is react. MutableRefObject.

Notice the HTMLInputElement type, which is a tag type, used to access DOM elements.

4. useCallback

Let’s look at the definition of useCallback in the type declaration file:

 function useCallback<T extends (. args:any[]) = >any> (callback: T, deps: DependencyList) :T;
 /** * `useMemo` will only recompute the memoized value when one of the `deps` has changed. * * Usage note: if calling `useMemo` with a referentially stable function, also give it as the input in * the second argument. * * ```ts * function expensive () { ... } * * function Component () { * const expensiveResult = useMemo(expensive, [expensive]) * return ... *} * ' '* *@version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#usememo
  */
Copy the code

UseCallback receives a callback function and an array of dependencies, and reexecutes the callback function only if the values in the array of dependencies change. Here’s an example:

const add = (a: number, b: number) = > a + b;

const memoizedCallback = useCallback(
  (a) = > {
    add(a, b);
  },
  [b]
);

Copy the code

Here we do not define a type for the argument a in the callback function, so the following calls will not fail:

memoizedCallback("hello");
memoizedCallback(5)
Copy the code

Even though both arguments to the add method are of type number, the above calls can be executed. So to be more precise, we need to define a specific type for the callback function:

const memoizedCallback = useCallback(
  (a: number) = > {
    add(a, b);
  },
  [b]
);
Copy the code

If you pass a string to the callback function, you will get an error:

Therefore, it is important to note that the parameters of the callback function need to be typed when using useCallback.

5. useMemo

Let’s look at the definition of useMemo in the type declaration file:

function useMemo<T> (factory: () => T, deps: DependencyList | undefined) :T;
   /**
    * `useDebugValue` can be used to display a label for custom hooks in React DevTools.
    *
    * NOTE: We don’t recommend adding debug values to every custom hook.
    * It’s most valuable for custom hooks that are part of shared libraries.
    *
    * @version 16.8.0
    * @see https://reactjs.org/docs/hooks-reference.html#usedebugvalue
    */
Copy the code

UseMemo is very similar to useCallback, but it returns a value instead of a function. Therefore, when defining useMemo, we need to define the type of return value:

let a = 1;
setTimeout(() = > {
  a += 1;
}, 1000);

const calculatedValue = useMemo<number> (() = > a ** 2, [a]);
Copy the code

If the return values are inconsistent, an error is reported:

const calculatedValue = useMemo<number> (() = > a + "hello", [a]);
// Parameters of type () => string cannot be assigned to parameters of type () => number
Copy the code

6. useContext

UseContext needs to provide a context object and return the value of the provided context, and when the provider updates the context object, the components that reference these context objects are rerendered:

const ColorContext = React.createContext({ color: "green" });

const Welcome = () = > {
  const { color } = useContext(ColorContext);
  return <div style={{ color}} >hello world</div>;
};
Copy the code

When useContext is used, the type of the provided context object is automatically inferred, so we do not need to manually set the type of the context. Currently, we can also use generics to set the type of the context:

interface IColor {
	color: string;
}

const ColorContext = React.createContext<IColor>({ color: "green" });
Copy the code

Here is the definition of useContext in the type declaration file:

function useContext<T> (context: Context<T>/*, (not public API) observedBits? : number|boolean */) :T;
/**
  * Returns a stateful value, and a function to update it.
  *
  * @version 16.8.0
  * @see https://reactjs.org/docs/hooks-reference.html#usestate
  */
Copy the code

7. useReducer

Sometimes we need to deal with complex states that may depend on previous states. You can use the useReducer, which receives a function that calculates a new state based on the previous state. The syntax is as follows:

const [state, dispatch] = useReducer(reducer, initialArg, init);
Copy the code

Consider the following example:

const reducer = (state, action) = > {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

const Counter = () = > {
  const initialState = {count: 0}
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({type: 'increment'})}>+</button>
      <button onClick={()= > dispatch({type: 'decrement'})}>-</button>
    </>
  );
}
Copy the code

The current state cannot be inferred, so you can add a type to the Reducer function and infer the type of the useReducer by defining state and action for the reducer function.

type ActionType = {
  type: 'increment' | 'decrement';
};

type State = { count: number };

const initialState: State = {count: 0}
const reducer = (state: State, action: ActionType) = > {
  // ...
}
Copy the code

This way, the type can be inferred in the Counter function. An error is reported when our view uses a type that does not exist:

dispatch({type: 'reset'});
// Error! type '"reset"' is not assignable to type '"increment" | "decrement"'
Copy the code

In addition, we can use the generic form to implement the reducer function type definition:

type ActionType = {
  type: 'increment' | 'decrement';
};

type State = { count: number };

const reducer: React.Reducer<State, ActionType> = (state, action) = > {
  // ...
}
Copy the code

The dispatch method also has types:

As you can see, the dispatch type is: react. dispatch.

import React, { useReducer } from "react";

type ActionType = {
  type: "increment" | "decrement";
};

type State = { count: number };

const Counter: React.FC = () = > {
  const reducer: React.Reducer<State, ActionType> = (state, action) = > {
    switch (action.type) {
      case "increment":
        return { count: state.count + 1 };
      case "decrement":
        return { count: state.count - 1 };
      default:
        throw new Error();
    }
  };

  const initialState: State = {count: 0}
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: "increment" })}>+</button>
      <button onClick={()= > dispatch({ type: "decrement" })}>-</button>
    </>
  );
};

export default Counter;

Copy the code

Iv. Event handling

1. Event Indicates the Event type

In development, we often use event event objects in event handlers, such as getting input values in real time on input fields; When using mouse events, get the coordinates of the current pointer through clientX, clientY, and so on.

As we know, Event is an object with many attributes, and many people define Event as any. TypeScript loses its meaning and does not statically check for events. There is no error if a keyboard Event fires one of the following methods:

const handleEvent = (e: any) = > {
	console.log(e.clientX, e.clientY)
}
Copy the code

Since the Event Event object has many attributes, it is not convenient to define all the attributes and their types in one interface. Therefore, React provides the type declaration of the Event Event object in the declaration file.

Common Event objects are as follows:

  • ClipboardEvent object: ClipboardEvent
  • DragEvent objects: DragEvent
  • FocusEvent object: FocusEvent
  • FormEvent object: FormEvent
  • Change event object: ChangeEvent
  • KeyboardEvent
  • MouseEvent object: MouseEvent
  • TouchEvent: TouchEvent
  • WheelEvent object: WheelEvent
  • AnimationEvent
  • TransitionEvent

As you can see, each of these Event Event objects receives a generic Element type, which is the type of the tag Element to which the Event is bound. The tag Element type is described in Part 5 below.

Here’s a simple example:

type State = {
  text: string;
};

const App: React.FC = () = > {  
  const [text, setText] = useState<string> ("")

  const onChange = (e: React.FormEvent<HTMLInputElement>): void= > {
    setText(e.currentTarget.value);
  };
  
  return (
    <div>
      <input type="text" value={text} onChange={this.onChange} />
    </div>
  );
}

Copy the code

Here we define the onChange event object to be of type FormEvent and use an input tag of type HTMLInputElement.

Look at the MouseEvent and ChangeEvent type declarations. Other event declarations look similar and similar:

interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
  altKey: boolean;
  button: number;
  buttons: number;
  clientX: number;
  clientY: number;
  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;
  movementX: number;
  movementY: number;
  pageX: number;
  pageY: number;
  relatedTarget: EventTarget | null;
  screenX: number;
  screenY: number;
  shiftKey: boolean;
}

interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
  target: EventTarget & T;
}
Copy the code

You can find EventTarget in many event object declaration files. This is because DOM event operations (listening and firing) are defined on the EventTarget interface. The EventTarget type declaration is as follows:


interface EventTarget {
    addEventListener(type: string.listener: EventListenerOrEventListenerObject | null, options? :boolean | AddEventListenerOptions): void;
    dispatchEvent(evt: Event): boolean;
    removeEventListener(type: string, listener? : EventListenerOrEventListenerObject |null, options? : EventListenerOptions |boolean) :void;
}
Copy the code

For example, in the change event, e.target is used to get the current value, which is of type EventTarget. Consider the following example:

<input
	onChange={e= > onSourceChange(e)}
	placeholder="Up to 30 words."
/>

const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) = > {
    if (e.target.value.length > 30) {
      message.error('Please do not exceed 30 words in length, please re-enter');
      return;
    }
    setSourceInput(e.target.value);
};
Copy the code

When the onChange event is triggered, the onSourceChange method is called. Its parameter e is of type react. ChangeEvent and e.target is of type EventTarget:

Here’s an example:

questionList.map(item= > (
    <div
    	key={item.id}
  		role="button"
  		onClick={e= >HandleChangeCurrent (item, e)} ></div>
)

const handleChangeCurrent = (item: IData, e: React.MouseEvent<HTMLDivElement>) = > {
    e.stopPropagation();
    setCurrent(item);
};
Copy the code

In this code, clicking on a box sets it to the current box, making it easier to do other things. When the mouse clicks on the box, the handleChangeCurren method is triggered. This method takes two arguments. The second argument is the event object. StopPropagation () is not a property of the MouseEvent MouseEvent, it is a property of the composite event. Let’s see the definition in the declaration file:

interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
	/ /...
}

interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
  / /...
}

interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}

interface BaseSyntheticEvent<E = object, C = any, T = any> {
  nativeEvent: E;
  currentTarget: C;
  target: T;
  bubbles: boolean;
  cancelable: boolean;
  defaultPrevented: boolean;
  eventPhase: number;
  isTrusted: boolean;
  preventDefault(): void;
  isDefaultPrevented(): boolean;
  stopPropagation(): void;
  isPropagationStopped(): boolean;
  persist(): void;
  timeStamp: number;
  type: string;
}
Copy the code

As you can see, stopPropagation() is inherited layer by layer, eventually from the BaseSyntheticEvent composite event type. The native set of events, syntheticEvents, is inherited from the self-synthesizing time type. SyntheticEvent<T = Element, E = Event> The generic interface accepts the current Element type and Event type.

<input 
  onChange={(e: SyntheticEvent<Element, Event>) = >{
    / /...}} / >Copy the code

2. Event handler type

After the event object types, let’s look at the event handler types. React also provides the type declarations for event handlers. Here’s a look at all the event handlers:

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void} ["bivarianceHack"];

type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
// Clipboard event handler
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
// Composite event handlers
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
// Drag the event handler
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
// Focus event handlers
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
// Form event handler
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
// Change event handler
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
// Keyboard event handler
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
// Mouse event handler
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
// Touch event handler
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
// pointer event handler
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
// interface event handlers
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
// Wheel event handler
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
// Animate the event handler
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
// Transition event handler
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
Copy the code

The type of T here is Element, which refers to the type of the HTML tag Element that triggers the event, as described in Part 5 below.

EventHandler receives an E, which represents the type of Event object in the Event handler. A bivarianceHack is a type definition of an Event handler that receives an Event object of the type of the received generic variable E and returns void.

Also look at the example above:

type State = {
  text: string;
};

const App: React.FC = () = > {  
  const [text, setText] = useState<string> ("")

  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) = > {
    setText(e.currentTarget.value);
  };
  
  return (
    <div>
      <input type="text" value={text} onChange={this.onChange} />
    </div>
  );
}

Copy the code

The onChange method is defined as a type of ChangeEventHandler, and the object is an input tag of type HTMLImnputElement.

5. HTML tag types

1. Common label types

The HTML tag related type declaration file can be found in the project dependencies file:

All HTML tag types are defined in the intrinsicElements interface. Common tags and their types are as follows:

a: HTMLAnchorElement;
body: HTMLBodyElement;
br: HTMLBRElement;
button: HTMLButtonElement;
div: HTMLDivElement;
h1: HTMLHeadingElement;
h2: HTMLHeadingElement;
h3: HTMLHeadingElement;
html: HTMLHtmlElement;
img: HTMLImageElement;
input: HTMLInputElement;
ul: HTMLUListElement;
li: HTMLLIElement;
link: HTMLLinkElement;
p: HTMLParagraphElement;
span: HTMLSpanElement;
style: HTMLStyleElement;
table: HTMLTableElement;
tbody: HTMLTableSectionElement;
video: HTMLVideoElement;
audio: HTMLAudioElement;
meta: HTMLMetaElement;
form: HTMLFormElement; 
Copy the code

When are tag types used? They are used in both the Event and Event handler types in Part 4 above. Many of the above types require passing in a generic parameter of type ELement. The generic parameter is the corresponding tag type value. You can select the tag type based on the tag. These types all inherit from the HTMLElement type. If you don’t need to use the type type, you can write HTMLElement directly. Take the following example:

<Button
	type="text"
	onClick={(e: React.MouseEvent<HTMLElement>) = > {
  handleOperate();
  e.stopPropagation();
}}
  >
    <img
			src={cancelChangeIcon}
			alt=""
		/>Unmodify </Button>Copy the code

In fact, label types are also used when working directly with the DOM, and although we now use frameworks for development, it is sometimes unavoidable to work directly with the DOM. For example, in my work, some components in a project are imported from other groups through NPM, and in many cases, I need to dynamically depersonalize the style of this component. The most direct way is to obtain DOM elements through native JavaScript to modify the style, which is when the label type is used.

Consider the following example:

document.querySelectorAll('.paper').forEach(item= > {
  const firstPageHasAddEle = (item.firstChild as HTMLDivElement).classList.contains('add-ele');
  
  if (firstPageHasAddEle) {
    item.removeChild(item.firstChild asChildNode); }})Copy the code

This is a piece of code I wrote recently (slightly modified) to remove the add-ele element on the first page. Item. firstChild is ChildNode. ChildNode does not have a classList attribute. When we assert it as an HTMLDivElement type, we will not report an error. Many times, label types can be used with assertions (as).

The as assertion is used in removeChild. Isn’t item.firstChild already automatically recognized as ChildNode? Because TS would think that we may not be able to get to a class called paper elements, so the item. The firstChild type is inferred for ChildNode | null, we sometimes more understand than TS defined elements, we know there must be a page paper elements, So you can directly assert item.firstChild as ChildNode.

2. Label attribute type

As we all know, each HTML tag has its own attributes, such as value, width, placeholder, max-length and other attributes.

interface InputHTMLAttributes<T> extendsHTMLAttributes<T> { accept? :string | undefined; alt? :string | undefined; autoComplete? :string | undefined; autoFocus? :boolean | undefined; capture? :boolean | string | undefined; checked? :boolean | undefined; crossOrigin? :string | undefined; disabled? :boolean | undefined; enterKeyHint? :'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined; form? :string | undefined; formAction? :string | undefined; formEncType? :string | undefined; formMethod? :string | undefined; formNoValidate? :boolean | undefined; formTarget? :string | undefined; height? :number | string | undefined; list? :string | undefined; max? :number | string | undefined; maxLength? :number | undefined; min? :number | string | undefined; minLength? :number | undefined; multiple? :boolean | undefined; name? :string | undefined; pattern? :string | undefined; placeholder? :string | undefined; readOnly? :boolean | undefined; required? :boolean | undefined; size? :number | undefined; src? :string | undefined; step? :number | string | undefined;
  type? :string | undefined; value? :string | ReadonlyArray<string> | number | undefined; width? :number | string | undefined; onChange? : ChangeEventHandler<T> |undefined;
}
Copy the code

If we need to manipulate the DOM directly, we may use element attribute types. Common element attribute types are as follows:

  • HTML attribute type: HTMLAttributes
  • Button attribute type: ButtonHTMLAttributes
  • Form attribute type: FormHTMLAttributes
  • Image attribute type: ImgHTMLAttributes
  • Input box attribute type: InputHTMLAttributes
  • Link attribute type: LinkHTMLAttributes
  • Meta attribute type: MetaHTMLAttributes
  • Select box attribute type: SelectHTMLAttributes
  • Table attribute type: TableHTMLAttributes
  • Input field attribute type: TextareaHTMLAttributes
  • Video attribute type: VideoHTMLAttributes
  • SVG attribute type: SVGAttributes
  • WebView attribute type: WebViewHTMLAttributes

In general, we rarely need to explicitly define the type of the tag attribute in the project. These attributes come into play if the child encapsulates the component library. Here’s an example (from the web, for learning purposes only) :

import React from 'react';
import classNames from 'classnames'

export enum ButtonSize {
    Large = 'lg',
    Small = 'sm'
}

export enum ButtonType {
    Primary = 'primary',
    Default = 'default',
    Danger = 'danger',
    Link = 'link'
}

interfaceBaseButtonProps { className? :string; disabled? :boolean; size? : ButtonSize; btnType? : ButtonType; children: React.ReactNode; href? :string;    
}

type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> // Use the cross type (&) to get our own attributes and the attributes of the native button
type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement> // Use the cross type (&) to get our own attributes and the attributes of the native A tag

export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> // Use Partial<> to make both properties optional

const Button: React.FC<ButtonProps> = (props) = > {
    const{ disabled, className, size, btnType, children, href, ... restProps } = props;const classes = classNames('btn', className, {
        [`btn-${btnType}`]: btnType,
        [`btn-${size}`]: size,
        'disabled': (btnType === ButtonType.Link) && disabled  // Only the A tag has the disabled class name, button does not
    })

    if(btnType === ButtonType.Link && href) {
        return (
            <a 
            	className={classes}
            	href={href}
            	{. restProps}
            >
                {children}
            </a>)}else {
        return (
            <button 
            	className={classes}
            	disabled={disabled} // buttonThe element defaults todisabledProperty, so it will be normal even if it is not set to stylebuttonThere are certain differences {. restProps}
            >
                {children}
            </button>
        )
    }
}

Button.defaultProps = {
    disabledfalse.btnType: ButtonType.Default
}

export default Button;
Copy the code

This code encapsulates a ButTom button and adds some custom attributes to the base of the button. For example, the type of the button is crossed (&) to obtain the custom attribute and the native button attribute:

type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> 
Copy the code

As you can see, tag attribute types are still useful for encapsulating component libraries

Tool generics

Using some tool generics in our projects can improve our development efficiency and reduce the number of type definitions we write. Here’s a look at some common tool generics and how they are used.

1. Partial

Partial is used to make an attribute passed in optional. This works when the type structure is unclear. It uses two keywords: keyof and in. Let’s see what they mean. Keyof can be used to retrieve all key values of an interface:

interface Person {
  name: string;
  age: number;
 	height: number;
}
type T = keyof Foo 
/ / T type is: "the name" | | "age" "number"
Copy the code

The in keyword traverses enumerated types, :

type Person = "name" | "age" | "number"
type Obj =  {
  [p in Keys]: any
} 
// Obj类型为: { name: any, age: any, number: any }
Copy the code

Keyof can produce associative types, and in can iterate over enumerated types, so they are often used together. Here is the Partial utility generic definition:

/** * Make all properties in T optional */
type Partial<T> = {
    [P inkeyof T]? : T[P]; };Copy the code

Here, keyof T retrieves the names of all the attributes of T, then iterates with in, assigning the values to P, and finally T[P] retrieves the values of the corresponding attributes. In the middle? Is used to set the property to optional.

The following is an example:

interface IPerson {
  name: string;
  age: number;
 	height: number;
}

const person: Partial<IPerson> = {
	name: "zhangsan";
}
Copy the code

2. Required

Required makes the attribute passed in mandatory. In contrast to the utility generics above, it is declared as follows:

/** * Make all properties in T required */
type Required<T> = {
    [P inkeyof T]-? : T[P]; };Copy the code

As you can see, this uses -? Make the property mandatory, which can be interpreted as subtracting the question mark. Apply Partial similar to the above:

interfaceIPerson { name? :string; age? :number; height? :number;
}

const person: Required<IPerson> = {
	name: "zhangsan";
  age: 18;
 	height: 180;
}
Copy the code

3. Readonly

Set all properties of type T to readonly. Properties of the constructed type cannot be reassigned. Readonly is declared as follows:

/** * Make all properties in T readonly */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
Copy the code

The following is an example:

interface IPerson {
  name: string;
  age: number;
}

const person: Readonly<IPerson> = {
	name: "zhangsan".age: 18
}

person.age = 20;  // Error: cannot reassign a readonly property
Copy the code

As you can see, the IPerson property has been converted to read-only by Readonly and cannot be assigned.

4. Pick<T, K extends keyof T>

The new type is constructed by picking part of the attributes K from type T. It is declared as follows:

/** * From T, pick a set of properties whose keys are in the union K */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
Copy the code

The following is an example:

interface IPerson {
  name: string;
  age: number;
 	height: number;
}

const person: Pick<IPerson, "name" | "age"> = {
	name: "zhangsan".age: 18
}
Copy the code

5. Record<K extends keyof any, T>

Record is used to construct a type with an attribute name of type K and an attribute value of type T. This utility generics can be used to map attributes of one type to another type. Here is how it is declared:

/** * Construct a type with a set of properties K of type T */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
Copy the code

The following is an example:

interface IPageinfo {
  name: string;
}

type IPage = 'home' | 'about' | 'contact';

const page: Record<IPage, IPageinfo> = {
		about: {title: 'about'},
  	contact: {title: 'contact'},
    home: {title: 'home'}},Copy the code

6. Exclude<T, U>

To Exclude is to Exclude a subset of one union type from another. This is how it is declared:

/** * Exclude from T those types that are assignable to U */
type Exclude<T, U> = T extends U ? never : T;
Copy the code

The following is an example:

interface IPerson {
  name: string;
  age: number;
 	height: number;
}

const person: Exclude<IPerson, "age" | "sex"> = {
	name: "zhangsan";
  height: 180;
}
Copy the code

7. Omit<T, K extends keyof any>

Pick and Exclude are both basic tool generics. In many cases, using Pick or Exclude is not as straightforward as writing types directly. Omit Omit is a more abstract encapsulation based on both, allowing multiple attributes to be culled from an object, leaving only new types to be desired. Here is how it is declared:

/** * Construct a type with the properties of T except for those in type K. */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Copy the code

The following is an example:

interface IPerson {
  name: string;
  age: number;
 	height: number;
}

const person: Omit<IPerson, "age" | "height"> = {
	name: "zhangsan";
}
Copy the code

8. ReturnType

ReturnType returns the type of the value returned by the function, declared as follows:

/** * Obtain the return type of a function type */
type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;
Copy the code

The following is an example:

function foo(type) :boolean {
  return type= = =0
}

type FooType = ReturnType<typeof foo>
Copy the code

Typeof is used here to get the function signature for foo, equivalent to (type: any) => Boolean.

7. Axios encapsulation

In the React project, we often use the Axios library for data requests. Axios is a Promise-based HTTP library that can be used in browsers and Node.js. Axios has the following features:

  • Create XMLHttpRequests from the browser;
  • Create HTTP requests from Node.js;
  • Support the Promise API;
  • Intercepting requests and responses;
  • Transform request data and response data;
  • Cancel the request;
  • Automatically convert JSON data;
  • The client supports XSRF defense.

I won’t go into the basic use of Axios. For better invocation and global interception, Axios is usually wrapped in TypeScript so that it also has good type support. Axios comes with its own declaration file, so we don’t need to do anything extra.

Here’s the basic encapsulation:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; // introduce axios and type declarations defined in node_modules/axios/index.ts

 // Define an interface request class that creates an AXIOS request instance
class HttpRequest {
  // The basic path to receive interface requests
  constructor(public baseUrl: string) { 
    this.baseUrl = baseUrl;
  }
  
  // Call this method of the instance when the interface is called, returning AxiosPromise
  public request(options: AxiosRequestConfig): AxiosPromise { 
    // Create an instance of axios, which is a function that contains multiple attributes
    const instance: AxiosInstance = axios.create() 
    // Merge the base path with the configuration passed in individually by each interface, such as urls, parameters, etc
    options = this.mergeConfig(options) 
    // Call the interceptors method to make the interceptor work
    this.interceptors(instance, options.url) 
    / / return AxiosPromise
    return instance(options) 
  }
  
  // Used to add global request and response interception
  private interceptors(instance: AxiosInstance, url? :string) { 
    // Request and response interception
  }
  
  // Used to merge base path configuration and interface configuration alone
  private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { 
    return Object.assign({ baseURL: this.baseUrl }, options); }}export default HttpRequest;
Copy the code

BaseUrl usually has different paths in development and production environments, so you can use different base paths depending on whether you are currently in development or production. This is written in a configuration file:

export default {
    api: {
        devApiBaseUrl: '/test/api/xxx'.proApiBaseUrl: '/api/xxx',}};Copy the code

Introduce this configuration in the file above:

import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config';
const apiBaseUrl = env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl;
Copy the code

You can then pass the apiBaseUrl parameter to HttpRequest as the default:

class HttpRequest { 
  constructor(public baseUrl: string = apiBaseUrl) { 
    this.baseUrl = baseUrl;
  }
Copy the code

Add a request interceptor and a response interceptor to the interceptors method, so that all interface requests are handled in the same way:

private interceptors(instance: AxiosInstance, url? :string) {
  	// Request interception
    instance.interceptors.request.use((config: AxiosRequestConfig) = > {
      // All configurations requested by the interface can be modified in axios.defaults
      return config
    },
    (error) = > {
      return Promise.reject(error)
    })
 	
  	// Response interception
    instance.interceptors.response.use((res: AxiosResponse) = > {
      const { data } = res 
      const { code, msg } = data
      if(code ! = =0) {
        console.error(msg) 
      }
      return res
    },
    (error) = > { 
      return Promise.reject(error)
    })
  }
Copy the code

The format of the data returned by all requests is the same, so you can define an interface to specify the data structure to be returned. You can define an interface:

export interface ResponseData {
  code: numberdata? :any
  msg: string
}
Copy the code

Let’s see how Axios wrapped in TypeScript can be used. We can start by defining a request instance:

import HttpRequest from '@/utils/axios'
export * from '@/utils/axios'
export default new HttpRequest()
Copy the code

The request class is imported, and an instance of this class is exported by default. Then create a login interface request method:

import axios, { ResponseData } from './index'
import { AxiosPromise } from 'axios'

interface ILogin {
  user: string;
  password: number | string
}

export const loginReq = (data: ILogin): AxiosPromise<ResponseData> => {
  return axios.request({
    url: '/api/user/login',
    data,
    method: 'POST'})}Copy the code

This encapsulates the login request method loginReq, whose parameter must be the type of the ILogin interface we defined. This method returns a Promise of type AxiosPromise, a type built into the AXIos declaration file that can be passed in a generic variable parameter that specifies the type of the data field in the returned result.

You can then call the login interface:

import { loginReq } from '@/api/user'

const Home: FC = () = > {
  const login = (params) = > {
  	loginReq(params).then((res) = > {
    	console.log(res.data.code)
  	})	
  }  
}
Copy the code

This way, when we call the loginReq interface, we are prompted that the parameter is of type ILogin and we need to pass in several parameters. The experience of writing code will be much better.

Eight. Other

1. import React

When using TypeScript in React projects, the normal component files have the.tsx suffix and the public method files have the.ts suffix. To import React into a. TSX file, do the following:

import * as React from 'react'
import * as ReactDOM from 'react-dom'
Copy the code

This is a future-oriented import if you want to use the following imports in your project:

import React from "react";
import ReactDOM from "react-dom";
Copy the code

You need to do the following configuration in the tsconfig.json configuration file:

"compilerOptions": {
    // Allow default imports from modules that do not have default exports.
    "allowSyntheticDefaultImports": true,}Copy the code

2. Types or Interfaces?

Can we use types or Interfaces to define types, so how do we choose between them? The suggestions are as follows:

  • Use interfaces when defining public apis (such as editing a library) to make it easy for users to inherit interfaces, which allows them to be extended using most declared merges.
  • When defining component properties (Props) and states (State), it is recommended to use Type because type is more restrictive.

Interface and Type are two different concepts in TS. However, in most React cases, interface and type can achieve the same function effect. The biggest differences between Type and interface are as follows: The type cannot be edited twice, and the interface can be extended at any time:

interface Animal {
  name: string
}

// Add a new color attribute to the original one
interface Animal {
  color: string
}

type Animal = {
  name: string
}
// Type does not support attribute extension
// Error: Duplicate identifier 'Animal'
type Animal = {
  color: string
}
Copy the code

Type is very useful for joint types, such as: type type = TypeA | TypeB. Interface, on the other hand, is better for declaring a dictionary class line and then defining or extending it.

3. Lazy loading type

If we want to use lazy loading on the React Router, React also provides us with lazy loading methods. Here’s an example:

export interface RouteType {
    pathname: string;
    component: LazyExoticComponent<any>;
    exact: boolean; title? :string; icon? :string; children? : RouteType[]; }export const AppRoutes: RouteType[] = [
    {
        pathname: '/login'.component: lazy(() = > import('.. /views/Login/Login')),
        exact: true
    },
    {
        pathname: '/ 404'.component: lazy(() = > import('.. /views/404/404')),
        exact: true}, {pathname: '/'.exact: false.component: lazy(() = > import('.. /views/Admin/Admin'))}]Copy the code

Here’s how lazy load types and lazy methods are defined in the declaration file:

type LazyExoticComponent<T extends ComponentType<any>> = ExoticComponent<ComponentPropsWithRef<T>> & {
  readonly _result: T;
};

function lazy<T extends ComponentType<any> > (
factory: () => PromiseThe < {default: T }>
) :LazyExoticComponent<T>;
Copy the code

Type assertion

Type Assertion can be used to manually specify the Type of a value. In the React project, assertions are useful. Sometimes inferred types are not really types, and many times we may know our code better than TS, so we can use assertions (using the AS keyword) to define a worthy type.

Consider the following example:

const getLength = (target: string | number) :number= > {
  if (target.length) { / / the error type "string | number" does not exist on the attribute "length"
    return target.length; // Error type "number" does not have attribute "length"
  } else {
    returntarget.toString().length; }};Copy the code

When TypeScript isn’t sure what type a variable of a union type is, it can only access properties or methods that are common to all types of the union type, so adding type definitions for parameters target and return values now causes an error. We can use an assertion to declare the target type to string:

const getStrLength = (target: string | number) :number= > {
  if ((target as string).length) {      
    return (target as string).length; 
  } else {
    returntarget.toString().length; }};Copy the code

Note that type assertion is not a type conversion, and it is not allowed to assert a type that does not exist in a union type.

Let’s look at another example where a method is called with an argument:This parameter may be undefined, but we know that the value must exist, so we can assert it as a number:data? .subjectId as number

In addition, the label type, component type, and time type mentioned above can be specified to some data using assertions, again depending on the actual business scenario.

Insight: Using type assertions really solves a lot of bugs in projects

5. Enumeration types

The use of enumerated types in a project is also important. Using enumerated types makes the code more extensible. When I want to change a property value, I don’t need to change the property globally, I just need to change the value in the enumeration. In general, it is best to create a new file that defines enumeration values for easy reference. About grammar here is not much of an enum type, you can refer to previous post: introduction to the TS article | explanation TypeScript enumerated type “.

So much for using TypeScript elegantly in the React project, and more to come. Give it a thumbs up if you think it’s good!