preface

For a long time, there were many friends around SSH who were confused about how to use TS in React. They began to hate TS and felt that all kinds of inexplicable problems reduced the efficiency of development.

In fact, if you can use it well, TS will only spend a little more time writing types in the first development, and it will play a magical role in subsequent maintenance and reconstruction. It is highly recommended to use it in long-term maintenance projects.

In fact, I have always known that there is a good memo in The English version, and I wanted to directly recommend it to my friends, but many people have a headache in English, but the Chinese version of the memo is clicked into this scene:

In that case, do it yourself. This memo is based on some examples from the original English version and some expansions.

Lead based

The prerequisites for reading this article are:

  • Be familiar with React.
  • Familiarize yourself with TypeScript types.
  • This article will focus on React Hook as an example, but of course most of the type knowledge is generic.

That said, this article focuses on the “React/TypeScript combo” rather than the basics, which can be learned by reading the documentation.

I also recommend the React and TypeScript sections in my advanced progression guide for the early intermediate front end, which I won’t cover here.

tool

  • TypeScript Playground with React: You can debug React + TypeScript online, only debugging types, not running code
  • Stackblitz: Cloud development tool that lets you run React code directly and preview it
  • Create React App TypeScript: uses scaffolding to generate React + TS projects locally

Choose the debugging tool you like best.

Component Props

Here are some common types of Props for defining functions:

The base type

type BasicProps = {
  message: string;
  count: number;
  disabled: boolean;
  /** Array type */
  names: string[];
  /** Is limited to the following two string literals */ with the "union type"
  status: "waiting" | "success";
};
Copy the code

Object type

type ObjectOrArrayProps = {
  /** If you do not need to use a specific attribute, you can specify an object as ❌ not recommended */
  obj: object;
  obj2: {}; / / same as above
  /** Object type with specific attributes ✅ */ is recommended
  obj3: {
    id: string;
    title: string;
  };
  /** object array 😁 Common */
  objArr: {
    id: string;
    title: string; } [];/** Key can be any string, and the value is limited to type MyTypeHere */
  dict1: {
    [key: string]: MyTypeHere;
  };
  dict2: Record<string, MyTypeHere>; Dict1 basically the same, uses TS built-in Record type.
}
Copy the code

Function types

type FunctionProps = {
  /** Arbitrary function types ❌ it is not recommended to specify parameters and return value types */
  onSomething: Function;
  /** Functions with no arguments do not require the return value 😁 */ is often used
  onClick: () = > void;
  /** the argument 😁 with a function is very common */
  onChange: (id: number) = > void;
  /** Another function syntax argument is the React button event 😁 which is very common */
  onClick(event: React.MouseEvent<HTMLButtonElement>): void;
  /** Optional parameter type 😁 Very common */optional? : OptionalType; }Copy the code

React related types

export declare interface AppProps {
  children1: JSX.Element; // ❌ is not recommended without considering arrays
  children2: JSX.Element | JSX.Element[]; // ❌ is not recommended without considering the string children
  children4: React.ReactChild[]; // Slightly better but null is not considered
  children: React.ReactNode; // ✅ contains all children cases
  functionChildren: (name: string) = > React.ReactNode; // ✅ returns the React node functionstyle? : React.CSSProperties;// ✅ is recommended for inline style
  // ✅ recommends all props types that come with the native Button tag
  // You can also pass in the component at the generic location to extract the Props type of the component
  props: React.ComponentProps<"button">;
  // ✅ we recommend using the previous step to further extract the native onClick function type
  // The first argument to the function is automatically inferred as the React click event typePonentProps < onClickButton:React.Com"button"> ["onClick"]}Copy the code

Functional component

The simplest:

interface AppProps = { message: string };

const App = ({ message }: AppProps) = > <div>{message}</div>;
Copy the code

2. Containing children:

With the react. FC built-in type, not only does it include the AppProps you defined, but it also automatically adds a children type, along with any other types that might appear in the component:

/ / is equivalent to
AppProps & { 
  children: React.ReactNode propTypes? : WeakValidationMap<P>; contextTypes? : ValidationMap<any>; defaultProps? : Partial<P>; displayName? :string;
}

/ / use
interface AppProps = { message: string };

const App: React.FC<AppProps> = ({ message, children }) = > {
  return (
    <>
     {children}
     <div>{message}</div>
    </>)};Copy the code

Hooks

The @types/ React package begins support for Hooks in versions 16.8 and older.

useState

If your default values already indicate the type, then instead of declaring the type manually, let TS infer the type automatically:

// val: boolean
const [val, toggle] = React.useState(false);

toggle(false)
toggle(true)
Copy the code

If the initial value is null or undefined, you need to manually pass in the desired type via generics.

const [user, setUser] = React.useState<IUser | null> (null);

// later...
setUser(newUser);
Copy the code

This also ensures that when you access a property on user directly, you are prompted that it may be NULL.

This error can be avoided by the optional chaining syntax (supported by TS 3.7 and up).

/ / ✅ ok
constname = user? .nameCopy the code

useReducer

The type of Action must be marked with Discriminated Unions.

const initialState = { count: 0 };

type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string };

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] = React.useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={()= > dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}
Copy the code

The Discriminated union is generally a union type. Each type is distinguished by a specific field, such as Type. When you pass ina specific type, the payload of the remaining types automatically matches and inferences.

Like this:

  • When you writetypeMatch to thedecrement“, TS will automatically infer the correspondingpayloadIt should bestringType.
  • When you writetypeMatch to theincrementWhen, thenpayloadIt should benumberType.

When you dispatch, enter the corresponding type and you will be reminded of the remaining parameter types.

useEffect

The main thing to note here is that useEffect is passed in a function that returns either a method (cleanup function) or undefined, otherwise it will return an error.

A more common case is when our useEffect needs to execute an async function, such as:

/ / ❌
// Type 'Promise<void>' provides no match 
// for the signature '(): void | undefined'
useEffect(async() = > {const user = await getUser()
  setUser(user)
}, [])
Copy the code

Although there is no explicit return value in async functions, async functions return a Promise by default, which causes TS to report an error.

It is recommended to rewrite:

useEffect(() = > {
  const getUser = async() = > {const user = await getUser()
    setUser(user)
  }
  getUser()
}, [])
Copy the code

Or use self-executing functions? Not recommended, not very readable.

useEffect(() = >{(async() = > {const user = await getUser()
    setUser(user)
  })()
}, [])
Copy the code

useRef

Most of the time, this Hook has no initial value, so that we can declare the type of the current property that returns the object:

const ref2 = useRef<HTMLElement>(null);
Copy the code

Take a button scenario as an example:

function TextInputWithFocusButton() {
  const inputEl = React.useRef<HTMLInputElement>(null);
  const onButtonClick = () = > {
    if(inputEl && inputEl.current) { inputEl.current.focus(); }};return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
Copy the code

When the onButtonClick event is triggered, you can be sure that inputEl also has a value because the components are rendered at the same level, but redundant non-null judgments are still made.

There’s a way around it.

const ref1 = useRef<HTMLElement>(null!). ;Copy the code

null! This syntax is a non-null assertion. Following a value indicates that you have concluded that it has a value, so TS does not give an error when you use inputel.current.focus ().

However, this syntax is dangerous and should be minimized.

In most cases, inputel.current? .focus() is a safer choice, unless the value really cannot be null. (for example, it was assigned before it was used)

useImperativeHandle

It is recommended to use a custom innerRef instead of the native ref because using the forwardRef can be very complicated.

typeListProps = { innerRef? : React.Ref<{ scrollToTop():void }>
}

function List(props: ListProps) {
  useImperativeHandle(props.innerRef, () = > ({
    scrollToTop(){}}))return null
}
Copy the code

Given what useRef has just learned, the use looks like this:

function Use() {
  const listRef = useRef<{ scrollToTop(): void} > (null!). useEffect(() = > {
    listRef.current.scrollToTop()
  }, [])

  return (
    <List innerRef={listRef} />)}Copy the code

Perfect, isn’t it?

You can debug the useImperativeHandle example online.

Also check out the useImperativeHandle discussion Issue, which has lots of interesting ideas, as well as complex examples of using the React.forwardRef.

Customize the Hook

If you want to return an array to the user like useState, be sure to use as const when appropriate, marking the return value as a constant, telling TS that the values in the array will not be deleted, order changed, etc…

Otherwise, each of your items will be inferred to be a “combined type of all type possibilities”, which will affect user usage.

export function useLoading() {
  const [isLoading, setState] = React.useState(false);
  const load = (aPromise: Promise<any>) = > {
    setState(true);
    return aPromise.finally(() = > setState(false));
  };
  [Boolean, typeof load] // ✅ adds as const
  / / ❌ or will be (Boolean | typeof load) []
  return [isLoading, load] as const;[]
}
Copy the code

By the way, if you’re writing a library with React Hook, don’t forget to export the types to the user as well.

React API

forwardRef

A functional component cannot append ref by default; it does not have its own instance like a class component. This API is typically used by functional components to receive refs from parent components.

So you need to label the instance type, which is what type of value the parent component can get through ref.

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

Since the ref is forwarded directly to the button in this example, just label the type as HTMLButtonElement.

The parent component is called like this to get the correct type:

export const App = () = > {
  const ref = useRef<HTMLButtonElement>()
  return (
    <FancyButton ref={ref} />)}Copy the code

thanks

This article uses many examples from react-typescript-Cheatsheets, along with some embellishes and examples of your own. Students who are good at English can also read this extension.

Welcome to “Front End from Advancement to admission.” If this post gets a good number of likes, I’ll keep updating this series.