Preface:

How to use TypeScript in React: This is probably the most complete TypeScript tutorial on the web. It’s about 1.7 words long, and includes a variety of techniques and techniques you can use in your daily development. This is a quick way to get started with TypeScript for those of you who are already familiar with TypeScript but won’t be able to use it in your projects, and almost any sample code you build will run on a basic scaffolding and be easy to understand. If you’re not already familiar with TS, take a look at my other great TypeScript article

In addition, I mainly refer to a few articles in the last, if you need to click the link to jump

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:

,>
,>

//App.tsx
import React from "react";

interface IProps {
  name: string;
}

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;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App name="qq" />
  </React.StrictMode>.document.getElementById("root"));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:

//App.tsx
import React from "react";
// Import type IProps
import {type IProps} from './index'
// Generics inherit imported types
 class MyComponent<P extends IProps> extends React.Component<P> {
  internalProp: P;
  constructor(props: P) {
    super(props);
    this.internalProp = props;
  }

  render() {
    console.log(this.props);
    console.log(this.internalProp);

    const { age ,name} = this.props;
    return (
      <div>
        {age}
        {name}
        <span>hello world </span>;
      </div>); }};export default MyComponent;

//index.tsx

import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
// Declare the type and export it
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />   // Success
    <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 
  </React.StrictMode>,
  document.getElementById("root")
);
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:

//app.tsx
import React from "react";
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;

//index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App name="qq" />
  </React.StrictMode>.document.getElementById("root"));Copy the code

When you define a function component in this form, the props has the children attribute by default, which represents the elements inside the component when it is called.

Complete the case

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <App name="zgc" />
  </React.StrictMode>.document.getElementById("root"));//App.tsx
import React from "react";
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;

//child1.tsx
interface IProps {
  name: string;
}
const Child1: React.FC<IProps> = (props) = > {
  const { name, children } = props;
   // I personally prefer to use the 'React.FC' approach to create type-constrained functional components, which also supports passing in 'children' even if it is not defined in our type
  console.log("child1", name, children);
  return (
    <div>
      <h1>hello child1</h1>
      <h2>{name}</h2>{children} // Show the last three lines in the figure below</div>
  );
};
export default Child1;

//child2.tsx
interface IProps {
  name: string;
}
const Child2: React.FC<IProps> = (props) = > {
  const { name } = props;
  return (
    <div>
      <h1>hello child2</h1>
      <h2>{name}</h2>
    </div>
  );
};
export default Child2;
Copy the code

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:

//App.tsx
import {type IProps} from './index'
// Define the component
function MyComponent<P extends IProps> (props: P) {
  const {name ,age } = props
  return <span>{name} {age}</span>;
}
export default MyComponent;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />; // Success
    <MyComponent<IProps> name="TypeScript" age="hello" />; // Error 
  </React.StrictMode>,
  document.getElementById("root")
);
Copy the code

Function components defined using arrow functions:

//App.tsx
import {type IProps} from './index'
// Define the component
const MyComponent = <P extends IProps >(props: P) =>{
  const {name ,age } = props
  return <span>{name} {age}</span>;
}
export default MyComponent;

//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import MyComponent from "./App";
export type IProps = { name: string; age: number };

ReactDOM.render(
  <React.StrictMode>
    <MyComponent<IProps> name="React" age={18} />; // Success
    {/* <MyComponent<IProps> name="TypeScript" age="hello" />; // Error */}
  </React.StrictMode>,
  document.getElementById("root")
);
Copy the code

In most cases, the React.FC declaration mode is the simplest and most effective. If type incompatibility occurs, you are advised to use the following two methods:

Second: use PropsWithChildren, which saves you from frequently defining the children type and automatically sets the children type to ReactNode:

type AppProps = React.PropsWithChildren<{ message: string }>

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

Third: Direct statement:

type AppProps = {
  message: string children? : React.ReactNode }const App = ({ message, children }: AppProps) = > (
  <div>
    {message}
    {children}

  </div>
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. React Prop Type

  • If you have a configurationEslintGeneral function components require you to define the type to return, or pass in someReactAssociated type attributes.

It’s important to understand some of the types exposed by React customizations. For example, the common React.ReactNode.

export declare interface AppProps {
    children1: JSX.Element; // ❌ bad, the array type is not considered
    children2: JSX.Element | JSX.Element[]; // ❌ does not consider character types
    children3: React.ReactChildren; // ❌ name bluff, tool type, use with caution
    children4: React.ReactChild[]; // better, but null is not considered
    children: React.ReactNode; // ✅ best, best accept all children types
    functionChildren: (name: string) = > React.ReactNode; // ✅ returns to the React nodestyle? : React.CSSProperties;// React styleonChange? : React.FormEventHandler<HTMLInputElement>;// Form event! The generic parameter is the type of 'event.target'
}
Copy the code

Third, the React of Hooks

If you are not familiar with React Hooks, read my article on React Hooks

1. useState

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

// 'val' is derived to Boolean, and toggle receives Boolean arguments

const [val, toggle] = React.useState(false)

// obj is automatically derived to type: {name: string}

const [obj] = React.useState({ name: 'sj' })

// arr is automatically derived to type: string[]

const [arr] = React.useState(['One'.'Two'])
Copy the code

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
  type ArticleInfo = {
    title: string;
    content: number;
  };

  const [article, setArticle] = useState<ArticleInfo>({ title:"zgc".content: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

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

You can think of useEffect as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount.

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 is allowed to return only one function, or undefined, otherwise an error will be reported.

Case study:

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.

Truth:

 // Get back-end data
  useEffect(() = > {
    // Prevent memory leaks
    let isUnmount = false;
    const dataList = async() = > {const {data: {item col: col}} =await getLists();
      if (!isUnmount) {
        setData(col);
      }
    };
    dataList();
    return () = > { // Return a function
      isUnmount = true; }; } []);Copy the code

3. useRef

When the initial value is null, there are two ways to create it:

const ref1 = React.useRef<HTMLInputElement>(null)

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

The difference between the two is:

  • The first method of ref1.current is read-only and can be passed to the built-in ref attribute, binding DOM elements;
  • The second way ref2.current is mutable (similar to declaring a member variable of a class)
const ref = React.useRef(0)

React.useEffect(() = > {
  ref.current += 1
}, [])
Copy the code

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:

import React from "react";

export default function App() {
  // Initialize to null, but tell TS that it wants HTMLInputElement type
  // inputEl can only be used with input elements
  const inputEl = React.useRef<HTMLInputElement>(null);
  const onButtonClick = () = > {
    // TS will check the inputEl type and initialize null to have no focus attribute on current
    // You need to customize the judgment!
    if (inputEl && inputEl.current) {
      inputEl.current.focus();
    }
    // Or: ✅ bestinputEl.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)

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:

import { useCallback, useState } from "react";
function App() {
  const add = (a: number, b: number) = > console.log(a + b);

  const [b, setb] = useState<number>();
  const memoizedCallback = useCallback(
    (a, b) = > {
      add(a, b);
    },
    [b]
  );
 
  return <div>1</div>;
}
export default App;
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".1); //hello1
  memoizedCallback(5.1);   / / 6
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, b: number) = > {
      add(a, b);
    },
    [b]
  );
Copy the code

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

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:

import { useMemo } from "react";
function App() {

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

  const calculatedValue = useMemo<number>(() = > a ** 2, [a]);
}
export default App;
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

Note: The generic type of useMemo specifies the return value type, and the generic type of useCallback specifies the parameter type

// You can also explicitly specify the return value type. Inconsistent returns will result in an error

const result = React.useMemo<string>(() = > 2[]),// Parameters of type () => number cannot be assigned to parameters of type () => string.

const handleChange = React.useCallback<React.ChangeEventHandler<HTMLInputElement>>(
      evt= > {
           console.log(evt.target.value)
      }, []
)
Copy the code

6. 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

Case 1:

import { useReducer } from "react";
type ActionType = {
  type: "increment" | "decrement";
};

type State = { count: number };

function reducer(state: State, action: ActionType) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

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

export default Counter;
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

Generic writing:

import React, { useReducer } from "react";

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

type State = { count: number };

const Counter: React.FC = () = > {
//const reducer = (state: State, action: ActionType) => {
  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

Case 2:

import { useReducer } from "react";
// ❌ bad, you may pass in an undefined type or an incorrect word, and you need to be compatible with different types of payload
// type ACTIONTYPE = { type: string; payload? : number | string };

/ / ✅ good
type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string }
  | { type: "initial" };

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) };
    case "initial":
      return { count: initialState.count };
    default:
      throw new Error();
  }
}
const initialState = { count: 0 };

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={()= > dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}
export default Counter;
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.

7. 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:

Case 1:

import React, { useContext } from "react";

const themes = {
  light: {
    foreground: "# 000000".background: "#eeeeee",},dark: {
    foreground: "#ffffff".background: "# 222222",}};const ThemeContext = React.createContext(themes.light);
//themes.light initial default value
function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
// Accept themes.dark passed in
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background.color: theme.foreground}} >
      I am styled by theme context!
    </button>
  );
}
export default App;
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 Ithemes {
  foreground: string;
  background: string;
}

const ThemeContext = React.createContext<Ithemes>(themes.light);
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

Case 2: useContext and useReducer are used together to manage global data flows

import React, { useContext, useReducer } from "react";
type ACTIONTYPE =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: string }
  | { type: "initial" };

interface AppContextInterface {
  state: typeof initialState;
  dispatch: React.Dispatch<ACTIONTYPE>;
}

const initialState = { count: 0 };

const AppCtx = React.createContext<AppContextInterface>({
  state: initialState,
  dispatch: (action) = > action,
});

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) };
    case "initial":
      return { count: initialState.count };
    default:
      throw new Error();
  }
}

const App = (): JSX.Element => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <AppCtx.Provider value={{ state.dispatch}} >
      <Counter />
    </AppCtx.Provider>
  );
};

function Counter() {
  const { state, dispatch } = useContext(AppCtx);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: "decrement", payload: "5" })}>
        -
      </button>
      <button onClick={()= > dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

export default App;
Copy the code

8. Customize Hooks

The beauty of Hooks is not just that they reduce the power of lines of code, but that they separate logic from THE UI. Do pure logic layer reuse.

  • Example: When you customize Hooks, return an array with elements of definite types, not union types. Const-assertions can be used.
export function useLoading() {
    const [isLoading, setState] = React.useState(false);
    const load = (aPromise: Promise<any>) = > {
        setState(true);
        return aPromise.finally(() = > setState(false));
    };
    return [isLoading, load] as const; / / infer [Boolean, typeof load], rather than the joint type (Boolean | typeof load) []
}
Copy the code
  • You can also assert thattuple typeTuple type.
export function useLoading() {
    const [isLoading, setState] = React.useState(false);
    const load = (aPromise: Promise<any>) = > {
        setState(true);
        return aPromise.finally(() = > setState(false));
    };
    return [isLoading, load] as [
        boolean, 
        (aPromise: Promise<any>) = > Promise<any>
    ];
}
Copy the code
  • If you need more than one of these, you can use generics to define a helper function and use TS autoinference.
function tuplify<T extends any[] > (. elements: T) {
    return elements;
}

function useArray() {
    const numberValue = useRef(3).current;
    const functionValue = useRef(() = > {}).current;
    return [numberValue, functionValue]; // type is (number | (() => void))[]
}

function useTuple() {
    const numberValue = useRef(3).current;
    const functionValue = useRef(() = > {
    }).current;
    return tuplify(numberValue, functionValue); // type is [number, () => void]
}
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:

import { useState } from "react";

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

  const onChange = (e: React.FormEvent<HTMLInputElement>): void= > {
    // console.log("e", e);
    // console.log("e.currentTarget", e.currentTarget);
    // console.log("e.target", e.target);
    // console.log(e.currentTarget === e.target); //true, where they are equal
    // e.currenttarget always refers to the element bound to the event, while e.target is the element triggered by the event.
    setText(e.currentTarget.value);
  };
  // console.log("text", text);
  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
    </div>
  );
};

export default App;
Copy the code

Here we define the onChange event object to be of type FormEvent, and the object is 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:

import { useState } from "react";

const App: React.FC = () = > {
  const [SourceInput, setSourceInput] = useState<string>("");

  const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) = > {
    if (e.target.value.length > 30) {
      console.log("Please do not exceed 30 characters in length. Please re-enter.");
      return;
    }
    setSourceInput(e.target.value);
  };
  console.log(SourceInput);

  return (
    <div>
      <input onChange={(e)= >OnSourceChange (e)} placeholder=" placeholder "/></div>
  );
};

export default App;
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 another example:

import React, { useState } from "react";

export default function App() {
  const [current, setCurrent] = useState<number> ();const handleChangeCurrent = (
    item: number,
    e: React.MouseEvent<HTMLDivElement>
  ) = > {
    e.stopPropagation(); // To prevent the event from bubbling, click on whatever it is, otherwise return the outermost value
    setCurrent(item);
  };
  console.log(current);

  return (
    <div
      onClick={(e)= > handleChangeCurrent(1, e)}
      style={{
        width: "100px",
        height: "100px",
        backgroundColor: "red",
      }}
    >
      1
      <div
        onClick={(e)= > handleChangeCurrent(2, e)}
        style={{
          width: "60px",
          height: "60px",
          backgroundColor: "blue",
        }}
      >
        2
        <div
          onClick={(e)= > handleChangeCurrent(3, e)}
          style={{
            width: "30px",
            height: "30px",
            backgroundColor: "green",
          }}
        >
          3
        </div>
      </div>
    </div>
  );
}

Copy the code

import React, { useState } from "react";

export default function App() {
  const onClickInner = (e: React.MouseEvent<HTMLDivElement>) = > {
    e.stopPropagation();
    console.log("inner div");
  };
  const onClickOuter = (e: React.MouseEvent<HTMLDivElement>) = > {
    console.log("outer div");
  };

  return (
    <div onClick={onClickOuter}>
      <div onClick={onClickInner}>inner div</div>
    </div>
  );
}

Copy the code

StopPropagation () is not a property of the 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
import * as React from "react";

const App: React.FC = () = > {
  const [state, setState] = React.useState("");

  const onChange = (e: React.SyntheticEvent<HTMLInputElement>) = > {
    setState(e.currentTarget.value);
  };

  return (
    <div>
      <input type="text" value={state} onChange={onChange} />
    </div>
  );
};
export default App;
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.

Case 1:

import React from "react";

export default function App() {
  const handleChangeCurrent: React.MouseEventHandler<HTMLDivElement> = (
    e: React.MouseEvent<HTMLDivElement>
  ) = > {
    console.log(e.clientX);
  };
  return <div onClick={(e)= > handleChangeCurrent(e)}>1</div>;
}
Copy the code

Case 2:

import * as React from 'react'

type changeFn = (e: React.FormEvent<HTMLInputElement>) = > void

const App: React.FC = () = > {
  const [state, setState] = React.useState(' ')

  const onChange: changeFn = e= > {
    setState(e.currentTarget.value)
  }

  return (
    <div>
      <input type="text" value={state} onChange={onChange} />
    </div>)}export default App
Copy the code

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;

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:

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'} interface BaseButtonProps { 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. But before I go into details, I’ll give you some basic information so you can learn more about the other tool types.

1. keyof typeof

In TypeScript, we often write ~ as in the following example

enum ColorsEnum {
    white = '#ffffff',
    black = '# 000000',
}

type Colors = keyof typeof ColorsEnum;
Copy the code

The last row is equivalent to

type Colors = "white" | "black"
Copy the code

So how does keyof Typeof work? Let’s start with the analysis of ~

To understand how keyof Typeof works in TypeScript, you first need to understand literal types and union of literal types. I’ll explain these concepts first. I’ll go into keyof and Typeof, respectively, and finally come back to enum to answer the above questions. This answer is a bit long, but the examples are easy to understand.

Literal types

Literal types in Typescript are the more specific string, number, or Boolean types. For example, “Hello World” is a string, but string is not “Hello World”, “Hello World” is a more concrete type of string, so it’s a literal type.

A literal type can be defined like this:

type Greeting = "Hello"
Copy the code

This means that the Greeting object can have only one string value, “Hello”, and no other string value, or any other value, as this code says:

let greeting: Greeting
greeting = "Hello" // OK
greeting = "Hi"    // Error: Type '"Hi"' is not assignable to type '"Hello"'
Copy the code

Literal types are not very useful on their own, but when combined with union types, Type aliases, and Type Guards, they become very powerful

Here is an example of a joint literal type:

type Greeting = "Hello" | "Hi" | "Welcome"
Copy the code

Now the value of the Greeting object can be “Hello”, “Hi”, or “Welcome”

let greeting: Greeting
greeting = "Hello"       // OK
greeting = "Hi"          // OK
greeting = "Welcome"     // OK
greeting = "GoodEvening" // Error: Type '"GoodEvening"' is not assignable to type 'Greeting'
Copy the code

keyofUsed alone

Assuming you now have a type T, keyof T will give you a new type, which is the associative literal type we mentioned earlier, and the literal types that comprise it are the property names of T. The resulting type is a subtype of the string.

For example, look at the following interface:

interface Person {
    name: string
    age: number
    location: string
}
Copy the code

Using keyof on the Person type results in a new type, as shown in the following code:

type SomeNewType = keyof Person
Copy the code

Types of joint SomeNewType is a literal (” name “| |” age “” location”), it is composed of the attributes of the Person type.

Now you can create objects of type SomeNewType:

let newTypeObject: SomeNewType
newTypeObject = "name"           // OK
newTypeObject = "age"            // OK
newTypeObject = "location"       // OK
newTypeObject = "anyOtherValue"  // Error... Type '"anyOtherValue"' is not assignable to type 'keyof Person'
Copy the code

typeofUsed alone

TypeScript adds Typeof methods that can be used in a type context to get the typeof a variable or property.

import React from "react";
export default function App() {
  interface Person {
    name: string;
    age: number;
  }

  const sem: Person = { name: "semlinker".age: 30 };

  return <div>1</div>;
}
/ / equivalent to the

import React from "react";
export default function App() {
 
  const sem = { name: "semlinker".age: 30 };

  type Sem = typeof sem; // type Sem = Person
  const lolo: Sem = { name: "lolo".age: 5 };
  return <div>1</div>;
}

Copy the code

In the above code, we get the typeof the sem variable through the typeof operator and assign it to the sem type variable, after which we can use the sem type

You can do the same for nested objects:

const kakuqo = {
    name: "kakuqo".age: 30.address: {
      province: 'fujian'.city: 'xiamen'   
    }
}

type Kakuqo = typeof kakuqo;
/ / equivalent to the
/* type Kakuqo = { name: string; age: number; address: { province: string; city: string; }; } * /
Copy the code

In addition to retrieving the structural typeof an object, the typeof operator can also be used to retrieve the typeof a function object, for example:

function toArray(x: number) :Array<number> {
return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]
Copy the code

keyof typeofAt the same time use

As you probably already know, the typeof operator gives you the typeof the object, and in the case of the Person interface above, we already know its type, so we just need to use the keyof operator on Person.

But what if we don’t know the type of the object, or if we have only one value, something like the following?

const bmw = { name: "BMW".power: "1000hp" }
Copy the code

This is where we need to use keyof Typeof together.

Typeof BMW gives you their type {name: string, power: string}

The keyof operator then gives you the joint literal type, as described in the following code:

type CarLiteralType = keyof typeof bmw

let carPropertyLiteral: CarLiteralType
carPropertyLiteral = "name"       // OK
carPropertyLiteral = "power"      // OK
carPropertyLiteral = "anyOther"   // Error... Type '"anyOther"' is not assignable to type '"name" | "power"'
Copy the code

inenumFor use onkeyof typeof

In Typescript, enUms are used as types at compile time to make constants type-safe, but they are treated as objects at run time. This is because when Typescript code is compiled into Javascript, it is converted to normal objects. So let’s go back to the first example where we asked the question:

enum ColorsEnum {
    white = '#ffffff',
    black = '# 000000',}Copy the code

Here ColorsEnum exists at runtime as an object, not a type, so we need to use both keyof Typeof operators together, as shown in the code below.

type Colors = keyof typeof ColorsEnum

let colorLiteral: Colors
colorLiteral = "white"  // OK
colorLiteral = "black"  // OK
colorLiteral = "red"    // Error... Type '"red"' is not assignable to type '"white" | "black"'
Copy the code

2. in

In is used to iterate over enumerated types:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
Copy the code

3. 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:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }

  type T = keyof IPerson;
  
  const a: T = "age";
  const b: T = "name";
  const c: T = "height";

  return <div>1</div>;
}

/ / T type is: "the name" | "age" | "height"
Copy the code

The in keyword traverses enumerated types, :

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }

  type T = keyof IPerson;
  / / T type is: "the name" | "age" | "height"
  type Obj = {
    [p in T]: any;
  };
  const a: Obj = {
    name: 1.age: 1.height: 1};console.log(a);
  return <div>1</div>;
}

// Obj is of type:
//type Obj = {
//name: any;
//age: any;
//height: 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:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  // You can't go without one
  const a: IPerson = {
    name: "zgc".age: 1.height: 1};// Optional, no less
  const b: Partial<IPerson> = {
    name: "zgc".age: 1.height: 1};console.log(a, b);

  return <div>1</div>;
}
Copy the code

4. 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:

import React from "react";

export default function App() { interface IPerson { name? : string; age? : number; height? : number; }// Optional, no less
  const a: IPerson = {
    name: "zgc".age: 1.height: 1};// You can't go without one
  const b: Required<IPerson> = {
    name: "zgc".age: 1.height: 1};console.log(a, b);

  return <div>1</div>;
}
Copy the code

5. 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:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Readonly<IPerson> = {
    name: "zgc".age: 1.height: 1};// person.age = 20; Error: Cannot reassign a readonly property cannot be modified
  const person2: IPerson = {
    name: "zgc".age: 1.height: 1}; person2.age =20; / / to modify

  console.log(person1,person2);

  return <div>1</div>;
}
Copy the code

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

6. Extract<T, U>

Take the common part of both Type Type and Union Type and return it as a new Type.

Here’s what the declaration looks like:

/** * Extract from T those types that are assignable to U */

type Extract<T, U> = T extends U ? T : never;

Copy the code

The following is an example:

export default function App() {
  type T0 = Extract<"a" | "b" | "c"."a">;
  // "a"
  type T1 = Extract<"a" | "b" | "c"."a" | "b">;
  // "a"|"b"
  type T2 = Extract<string | number | (() = > void), Function>;
  // (() => void)

  const person: T0 = "a";

  console.log(person);

  return <div>1</div>;
}

Copy the code

7. Exclude<T, U>

  • Function: take the complement of the intersection of T and U in T

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:

export default function App() {
  type T0 = Exclude<"a" | "b" | "c"."a">;      
  // "b" | "c"
  type T1 = Exclude<"a" | "b" | "c"."a" | "b">;     
  // "c"
  type T2 = Exclude<string | number | (() = > void), Function>; 
  // string | number

  const person:T0 ='b' 
  
  console.log(person);

  return <div>1</div>;
}
Copy the code

8. Pick<T, K extends keyof T>

A new type is constructed by selecting part of the attribute K from type T (as opposed to omit and pick), which is declared in the following form:

/** * 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:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Pick<IPerson, "name" | "age"> = {
  // Select the options you need
    name: "zgc".age: 1};// Cannot have height
  
  const person2: IPerson = {
    name: "zgc".age: 1.height: 1};console.log(person1, person2);

  return <div>1</div>;
}
Copy the code

9. 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:

import React from "react";

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  const person1: Omit<IPerson, "age" | "height"> = {
    // Eliminate the options you don't need
    name: "zgc"};const person2: IPerson = {
    name: "zgc".age: 1.height: 1};console.log(person1, person2);

  return <div>1</div>;
}
Copy the code

Omit, Pick, Exclude similarities and differences:

  • Similarities:

    • All three utility classes use existing types for attribute filtering to obtain new types
    • Omit and Exclude are both used to Omit attributes
    • Omit and Pick arguments are of type <{key:value},key>.
  • Difference:

    • Omit may be literal or of specific types such as String, Boolean, etc. The second parameter of Omit and Pick must be a subattribute of the first parameter
  • Omit anything and Omit anything. Omit anything and Omit anything. Then Omit anything and Omit anything. Omit=Pick

    > T:

    K:key
    ,value>
    ,exclude

10. 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. Utility generics can be used to map an attribute of one type to another.

/** * 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:

export default function App() {
  interface IPerson {
    name: string;
    age: number;
    height: number;
  }
  type IPage = "home" | "about" | "contact";
  //const page: Record<K, T> = {
  //const page: Record< attribute name, attribute value > = {
  / / need to satisfy the constraints of k "string | number | symbol".
  const page: Record<IPage, IPerson> = {
    about: { name: "zgc".age: 1.height: 1 },
    contact: { name: "wf".age: 1.height: 1 },
    home: { name: "wlc".age: 1.height: 1}};console.log(page);
  return <div>1</div>;
}
Copy the code

11. 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> = Textends(... args: any) => infer R ? R : any;Copy the code

The following is an example:

import React from "react";

export default function App() {
  function foo(type: number) :boolean {
    return type === 0;
  }

  type FooType = ReturnType<typeof foo>;
  const a: FooType = true;
  console.log(a);

  return <div>1</div>;
}
Copy the code

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

  • Type can return subproperty types by index
function foo() {
  return {
    a: 1.b: 2.subInstArr: [1.2]}; } type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType["subInstArr"];
type SubIsntType = SubInstArr[0];

const baz: SubIsntType = 3;

// It can also be done in one step
type SubIsntType2 = ReturnType<typeof foo>["subInstArr"] [0];
const baz2: SubIsntType2 = 1;
console.log(baz, baz2);

function Counter() {
  return <></>;
}

export default Counter;
Copy the code

12. NonNullable

Filter out null and undefined in Type, and return the remaining Type as a new Type. It’s actually a special case of Exclude.

Source code analysis:

type NonNullable<T> = T extends null | undefined ? never : T
Copy the code

Can be found and Exclude < U > T, source code is very like, just change U to null | is undefined. So the combination of Exclude<T, U> makes sense.

Actual usage:

type T0 = NonNullable<string | number | undefined>;

// type T0 = string | number

type T1 = NonNullable<string[] | null | undefined>;

// type T1 = string[]
Copy the code

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: number data? : anymsg: 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 consumers to inherit interfaces, which allows them to be used and extended by declaring 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 React projects, assertions are still 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.

TypeScript adds enumerations to ES to make it possible to give names to sets of values in TypeScript, which is developer-friendly to understand that enumerations are dictionaries. Enum types are defined using enum:

enum Day {
  SUNDAY,
  MONDAY,
  TUESDAY,
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY
 }
Copy the code

The enumerated type Day defined above has seven values, and TypeScript assigns numbers to each of them, starting at 0 by default. When used, names can be used without remembering the relationship between numbers and names:

enum Day {
  SUNDAY = 0,
  MONDAY = 1,
  TUESDAY = 2,
  WEDNESDAY = 3,
  THURSDAY = 4,
  FRIDAY = 5,
  SATURDAY = 6
}
Copy the code

Here is what the above code looks like when translated into JavaScript:

var Day = void 0;
(function (Day) {
  Day[Day["SUNDAY"] = 0] = "SUNDAY";
  Day[Day["MONDAY"] = 1] = "MONDAY";
  Day[Day["TUESDAY"] = 2] = "TUESDAY";
  Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
  Day[Day["THURSDAY"] = 4] = "THURSDAY";
  Day[Day["FRIDAY"] = 5] = "FRIDAY";
  Day[Day["SATURDAY"] = 6] = "SATURDAY";
})(Day || (Day = {}));
Copy the code

As you can see, each value is assigned a corresponding number.

In TypeScript, we need to get members of an enumerated collection as points:

console.log(Day.SUNDAY)   / / 0
console.log(Day.MONDAY)   / / 1
Copy the code

With the basic use of enumerations out of the way, let’s take a look at common enumerations.

1. Enumeration of numbers

In the above example, you define a set of numbers that starts at 0 by default, called a numeric enumeration, by specifying only constant names. If you want to increment from another value, you can specify the index of the first value:

enum Color {
  Red = 2,
  Blue,
  Yellow
}
console.log(Color.Red, Color.Blue, Color.Yellow); / / 2, 3, 4
Copy the code

If you can specify an index value for a field, all fields that do not have an index value will be incremented by one:

// Specify some fields, others use default increment indexes
enum Status {
  Ok = 200,
  Created,
  Accepted,
  BadRequest = 400,
  Unauthorized
}
console.log(Status.Created, Status.Accepted, Status.Unauthorized); / / 201 202 401
Copy the code

In addition, it is possible to assign each field an arbitrary index value that is not contiguous:

enum Status {
  Success = 200,
  NotFound = 404.Error = 500
}
console.log(Status.Success, Status.NotFound, Status.Error); / / 200 404 500
Copy the code

Numeric enumerations can use computed values and constants when defining values. Note, however, that if a field uses a calculated value or constant, then the field immediately following that field must be set to its initial value. The default increment value cannot be used here.

// The initial value is calculated
const getValue = () = > {
  return 0;
};
enum ErrorIndex {
  a = getValue(),
  b, // The error enumerator must have an initialized value
  c
}
enum RightIndex {
  a = getValue(),
  b = 1,
  c
}
// The initial value is constant
const Start = 1;
enum Index {
  a = Start,
  b, // The error enumerator must have an initialized value
  c
}
Copy the code

2. Enumeration of strings

TypeScript calls enumerations that define values as string literals string enumerations require that each field’s value be either a string literal or another string enumerator of that enumerated value:

// Use string literals
enum Message {
  Error = "Sorry, error",
  Success = "Hoho, success"
}
console.log(Message.Error); // 'Sorry, error'

// Use other enumerators in enumeration values
enum Message {
  Error = "error message",
  ServerError = Error,
  ClientError = Error
}
console.log(Message.Error); // 'error message'
console.log(Message.ServerError); // 'error message'
Copy the code

Note that other enumerators here refer to enumerators in the same enumeration value, because string enumerations cannot use constants or computed values, so members in other enumerations cannot be used.

3. Reverse mapping

When you define an enumeration value, you can obtain the corresponding value in the form of Enum[‘key’] or Enum. Key. TypeScript also supports reverse mapping, but only numeric enumerations, not string enumerations. Consider the following example:

enum Status {
  Success = 200,
  NotFound = 404.Error = 500
}
console.log(Status["Success"]); / / 200
console.log(Status[200]); // 'Success'
console.log(Status[Status["Success"]]); // 'Success'
Copy the code

Enumerations defined in TypeScript are actually an object when compiled. In the generated code, enumerations are compiled into an object that contains both a forward mapping (name -> value) and a reverse mapping (value -> name). Let’s see what Status looks like when compiled in the code above:

{
    200: "Success".404: "NotFound".500: "Error".Error: 500.NotFound: 404.Success: 200
}
Copy the code

As you can see, TypeScript adds the field name of the enumerated value as the property name and the property value of the object, and the field value of the enumerated value as the property name and name of the object. This can be done either by enumerating the value’s field name or by enumerating the value’s value to the field name.

4. Heterogeneous enumeration

Heterogeneous enumeration is an enumeration value whose member values have both numeric and string types, as follows:

enum Result {
  Faild = 0,
  Success = "Success"
}
Copy the code

Asynchronous enumerations are not recommended during development. Because when a class of values is organized into an enumerated value, they tend to have similar characteristics. For example, when doing interface request, the return status code, if the status code is numeric, if it is a prompt message, are strings, so in the use of enumeration, often can avoid the use of heterogeneous enumeration, mainly to do a good job of sorting out the type.

5. Constant enumeration

In TypeScript, after enumeration values are defined, the compiled JavaScript code creates a corresponding object that can be used at runtime. But what if you use enumerations just to make your program readable and don’t need the compiled object? This will increase the amount of compiled code. In TypeScript, we have a const enum. We use the const keyword before the statement defining the enumeration. This way the compiled code does not create the object, but replaces it with the corresponding value from the enumeration:

enum Status {
  Off,
  On
}
const enum Animal {
  Dog,
  Cat
}
const status = Status.On;
const animal = Animal.Dog;
Copy the code

When compiled into JavaScript, the code above looks like this:

var Status;
(function(Status) {
  Status[(Status["Off"] = 0)] = "Off";
  Status[(Status["On"] = 1)] = "On";
})(Status || (Status = {}));
var status = Status.On;
var animal = 0; // Dog 
Copy the code

Status[” Off “] = 0 sets the Off attribute to the Status object, and the value is set to 0. This assignment expression returns the value on the right-hand side of the equals sign, which is 0, so Status[Status[“Off”] = 0] = “Off” equals Status[0] = “Off”. After the object is created, assign the value of Status’s On property to Status; Instead of creating an animal object like Status, the compiled code replaces the value 0 for animal. Dog with the animal. Dog position of the const animal = animal. Dog expression.

By defining constant enumerations, you can maintain an associated set of constants in a clear, structured form. And because the definition and inline member values are erased after translation, the code is no worse in terms of size and performance than the directly inline constant values.

6. Enumerator and joint enumerator types

If all the members of an enumeration value are literal-type values, then each member of the enumeration and the enumeration value itself can be used as a type. We call such enumerators literal enumerators. There are three types of values that satisfy the criteria for an enumerator:

  • An enumerator with no initial value, for example:enum E { A }
  • The value is a string literal, for example:enum E { A = 'a' }
  • Values are numeric literals or with-A numeric literal of a symbol, for example:enum E { A = 1 },enum E { A = -1 }
(1) Enumerator type

An enumerator becomes a type when all enumerators have literal enumerations:

enum Animal {
  Dog = 1,
  Cat = 2
}

interface Dog {
  type: Animal.Dog; 
}
interface Cat {
  type: Animal.Cat; 
}

let cat: Cat = {
  type: Animal.Dog // error [ts] Cannot assign type 'animal. Dog' to type 'animal. Cat'
};
let dog: Dog = {
  type: Animal.Dog
};
Copy the code

As you can see, line 7 of the code uses animal. Dog as the type, specifying that the interface Dog must have a type field with type animal. Dog.

(2)Joint enumerated type

When an enumeration value meets the criteria, the enumeration value can be treated as a union type containing all members:

enum Status {
  Off,
  On
}
interface Light {
  status: Status;
}
enum Animal {
  Dog = 1,
  Cat = 2
}
const light1: Light = {
  status: Animal.Dog // Error cannot assign type 'animal. Dog' to type 'Status'
};
const light2: Light = {
  status: Status.Off
};
const light3: Light = {
  status: Status.On
};
Copy the code

The above example defines the type of the status field of the interface Light as the enumeration value status, then the attribute value of status must be either status. Off or status. On, which is equivalent to status: Status. Off | Status. On.

7. Enumeration merge

With common enumeration types out of the way, let’s finally look at the concept of enumeration merging. For values of enumerated types, we can declare them separately:

enum Day {
  SUNDAY,
  MONDAY,
  TUESDAY
 }

enum Day {
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY
 }
Copy the code

TypeScript then merges the enumerated value and compiles it to JavaScript as follows:

var Day = void 0;
(function (Day) {
  Day[Day["SUNDAY"] = 0] = "SUNDAY";
  Day[Day["MONDAY"] = 1] = "MONDAY";
  Day[Day["TUESDAY"] = 2] = "TUESDAY";
  Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
  Day[Day["THURSDAY"] = 4] = "THURSDAY";
  Day[Day["FRIDAY"] = 5] = "FRIDAY";
  Day[Day["SATURDAY"] = 6] = "SATURDAY";
})(Day || (Day = {}));
Copy the code

8. Application: Eliminate magic numbers/characters

I hate some code points.

  • Bad example, see the following code does not know your heart, there is no alpaca galloping.
if (status === 0) {
    // ...
} else {
    // ...
}

// ...

if (status === 1) {
    // ...
}
Copy the code
  • Use enumerations to unify annotations and semantics
// enum.ts
export enum StatusEnum {
    Doing,   / /
    Success, / / success
    Fail,    / / fail
}

//index.tsx
if (status === StatusEnum.Doing) {
    // ...
} else {
    // ...
}

// ...

if (status === StatusEnum.Success) {
    // ...
}
Copy the code

9. Extension: Policy mode eliminates if and else

// Object constants
export const StatusEnum = {
    Doing: 0./ /
    Success: 1./ / success
    Fail: 2./ / fail
};

Copy the code
if (status === StatusEnum.Doing) {
    return 'in progress';
} else if (status === StatusEnum.Success) {
    return 'success';
} else {
    return 'failure';
}
Copy the code
  • The strategy pattern
// Object constants
export const StatusEnumText = {
    [StatusEnum.Doing]: 'in progress',
    [StatusEnum.Success]: 'success',
    [StatusEnum.Fail]: 'failure'};// ...
return StatusEnumText[status];
Copy the code

6. Type of Promise

We often use async functions for asynchronous operations, which return a Promise object when called. We can add callbacks using the then method.

Promise

is a generic type, and the T generic variable is used to determine the parameter type of the first callback function (onfulfilled) received when using the THEN method.

Example:


interface IResponse<T> {
  message: string,
  result: T,
  success: boolean,
}
async function getResponse () :Promise<IResponse<number[] > >{
  return {
    message: 'Obtain success'.result: [1.2.3].success: true,
  }
}
getResponse()
  .then(response= > {
    console.log(response.result)
  })
Copy the code

We first declare a generic interface for IResponse to define the type of Response, and use the T generic variable to determine the type of result.

We then declare an asynchronous function getResponse and define the type of the return value of the function as Promise

>.

Finally, calling the getResponse method returns a promise, called through then. The then method receives the first callback function response of type {message: string, result: Number [], success: Boolean}.

Promise < T > implementation source node_modules/typescript/lib/lib. Es5. Which s.

interface Promise<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */then<TResult1 = T, TResult2 = never>(onfulfilled? : ((value: T) = > TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected? : ((reason: any) = > TResult2 | PromiseLike<TResult2>) | undefined | null) :Promise<TResult1 | TResult2>;
    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch<TResult = never>(onrejected? : ((reason: any) = > TResult | PromiseLike<TResult>) | undefined | null) :Promise<T | TResult>;
}
Copy the code

7. forwardRef

A functional component cannot append ref by default; it does not have its own instance like a class component. The forwardRef is a functional component that receives a ref from the parent.

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

Child components:

export 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

8. Obtain the unexported Type

In some cases, when importing a third-party library for a component that does not export the required component parameter types, you can use ComponentProps to obtain the required types.

// Get the parameter type

import { Button } from 'library' // The props type is not exported

type ButtonProps = React.ComponentProps<typeof Button> // Get the props type

// Then you can use it

type AlertButtonProps = Omit<ButtonProps, 'onClick'> / / remove the onClick

const AlertButton: React.FC<AlertButtonProps> = props= > (
  <Button onClick={()= >alert('hello')} {... props} />
)
Copy the code

Reference article links:

  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 702167…
  • Juejin. Cn/post / 691892…
  • Juejin. Cn/post / 691086…
  • Juejin. Cn/post / 702323…