The original:
Notes on TypeScript: React and Generics


By A. Sharif


Translator: Bo Xuan

introduce

These notes should help you understand TypeScript better and find out how to use TypeScript in certain situations. All examples are based on TypeScript 3.2.

The generic

If you’ve been reading the TypeScript Notes series, you’ve seen widespread use of generics by now. Although we use generics all the time, we haven’t really introduced generics and what they can do. In this part of the series, we’ll first try to better understand the concepts of generics, and then see how they work better with React and TypeScript.

When we write software, we want some functionality to be reusable, without having to write specific functionality for every possible input type. Let’s take this example as a starting point:

function isDefinedNumber(a: number) : boolean {
  returna ! = =null|| a ! = =undefined;
}

function isDefinedString(a: string) : boolean {
  returna! = =null|| a ! = =undefined;
}Copy the code

Instead of writing an explicit function that takes string and number as input, we’ll write a function with the following signature:

function isDefined<Type>(a: Type) : boolean {
  return a! == null || a ! == undefined;
}Copy the code

IsDefined expects the generic Type Type to be entered. TypeScript tries to infer parameters and specify a correct type. Let’s move on to the next example and infer the type of return:

function of<Type>(a: Type) : Type[] {
  return [a];
}

const toNumbers = of(1); // const toNumbers: number[]
const toStrings = of("Test Of"); // const toString: string[]Copy the code

In the OF example, we can see that we don’t even need to define types because TypeScript can infer parameter types. This is not true in all cases, and sometimes we must be explicit about the type. We could also define the above function as follows:

const toNumbers = of<number> (1); // const toNumbers: number[]
const toStrings = of<string> ("Test Of"); // const toString: string[]Copy the code

Technically, we can use any:

function of(a: any) : any {
  if(a.length ! = =undefined) {
    return a
  }
  return a;
}Copy the code

But there is a big difference between using any and generics. If you look closely at the example above, we know nothing about the input parameters. Calling of with undefined or null will cause an error. Generics infer the exact type and force the input to be handled accordingly within the function body. Here is the same example using generics:

function of<Type>(a: Type) : Type[] {
  if(a.length ! == undefined) { //error: Property 'length' does not exist on 'Type'
    return a
  }
  return [a];
}Copy the code

We need to be more explicit when dealing with generics, and we can rewrite the example as follows:

function of<Type>(a: Type | Type[]) : Type[] {
  if (Array.isArray(a)) {
    return a
  }
  return [a];
}


const a = of(1); // const a: number[]
const b = of([1]); // const b: number[]Copy the code

Generics can also be used for reuse functions, such as the argument a can correspond to Type Type or Type array Type. The generic Type is bound to the number Type when 1 is passed as an argument, and the same happens when [1] is passed, where Type is bound to number.

So far we’ve seen how to use generics in functions, but at the same time, we can also use generics with classes, which can be very interesting when you write class components in React.

class GenericClass<Type> {
  of = (a: Type | Type[]) :Type[] = > {if (Array.isArray(a)) {
      return a;
    }
    return [a];
  };
}

const genericClass = new GenericClass<number>();
const a = genericClass.of(1); // const a: number[]
const b = genericClass.of("1"); // error!
const c = genericClass.of([1]); // const c: number[]Copy the code

The examples we’ve seen so far should help us understand the basics, and we’ll use generics with the React component based on this knowledge.

The React with generics

With React, we might have a function component where we need to infer the parameter types. This component takes a number or string argument, or a number or string array argument.

type RowProps<Type> = {
  input: Type | Type[];
};

function Rows<Type>({input}: RowProps<Type>) {
  if (Array.isArray(input)) {
    return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
  }
  return <div>{input}</div>
}

// usage

<Rows input={[1]} />
<Rows input={1} />
<Rows input={true} /> // Also works!Copy the code

That’s fine, but the constraint still applies to any value. We can pass true and TypeScript won’t complain. We need to strictly restrict Type or have Type inherit from string or number.

function Rows<Type extends number | string>({input}: RowProps<Type>) {
  if (Array.isArray(input)) {
    return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
  }
  return <div>{input}</div>
}

<Rows input={[1]} />
<Rows input={1} />
<Rows input="1" />
<Rows input={["1]} "/ >
<Rows input={true} /> //Error!Copy the code

We can ensure that only parameters of the expected type are now passed in. It is worth noting that we could also define a generic type for Props, as in the example above:

type RowProps<Type> = {
  input: Type | Type[];
};Copy the code

Next, we’ll build a more advanced example to see why generics can help us build reusable React components. We will build a component that expects two different inputs. Based on these inputs, we will evaluate a third value and a new value based on the original input, providing props for the render function.

type RenderPropType<InputType.OtherInputType> = { c: number } & InputType &
  OtherInputType;

type RowComponentPropTypes<InputType.OtherInputType> = {
  input: InputType;
  otherInput: OtherInputType;
  render: (props: RenderPropType<InputType.OtherInputType= > >)JSX.Element;
};Copy the code

The first step is to define RowComponentPropTypes. We infer the types of parameters in TypeScript and bind the RenderPropType to the props of the render function. RenderPropType merges the intersection of the new {c: number} type, which we will calculate together, InputType and OtherInputType. So far, we’ve been using generics a lot.

We may not know the exact type of input, so our next step is to restrict the type of input in the component.

class RowComponent<
  InputType extends { a: number },
  OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
  // implementation...
}Copy the code

By using InputType extends {a: Number} we ensure that our input has an A property that provides type number. This rule also applies to OtherInputType. Now we implement the RowComponent and make sure we provide a, B, and C attributes to the Render function.

Finally, here is our complete example:

class RowComponent<InputType extends { a: number }, OtherInputType extends { b: number }> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
  convert = (input: InputType, output: OtherInputType) => {
    return{ c: input.a + output.b, ... input, ... output }; }; render() {return this.props.render(
      this.convert(this.props.input, this.props.otherInput)
    );
  }
}

<RowComponent
  input={{ a: 1 }}
  otherInput={{ b: 2 }}
  render={({ a, b, c }) => (
    <div>
      {a} {b} {c}
    </div>
  )}
/>Copy the code

We now have a general idea of generics and how React works with TypeScript.

This article has contacted the author of the original, and authorized the translation, please keep the original link for reprinting