DEV Community

A. Sharif
A. Sharif

Posted on

Notes on TypeScript: React and Generics

Introduction

These notes should help in better understanding TypeScript and might be helpful when needing to lookup up how leverage TypeScript in a specific situation. All examples are based on TypeScript 3.2.

Generics

If you have been reading along the "Notes on TypeScript" series, then you will have seen extensive usage of generics so far. While we have been using generics, we haven't actually talked about generics and why they are useful. In this part of the series, we will first try to better understand the generics topics and then see how we can leverage generics when working with React and TypeScript.

When writing software, one aspect is that we want to be able to reuse some functionality without having to write a specific functionality for every possible input type. Let's take the following example as a starting point:

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

function isDefinedString(a: string) : boolean {
  return a!== null || a !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

We wouldn't write explicit functions for string or number inputs, rather we would write a function with the following signatures:

function isDefined<Type>(a: Type) : boolean {
  return a!== null || a !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

isDefined expects an input of generic Type. TypeScript will try to infer the argument and assign the correct type. Let's continue with another example, where we want to infer the return type:

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

const toNumbers = of(1); // const toNumbers: number[]
const toStrings = of("Test Of"); // const toString: string[]
Enter fullscreen mode Exit fullscreen mode

In the of example, we can see that we don't even need to define the type, as TypeScript can infer the argument type. This is not applicable in all cases, sometimes we have to be explicit about the type. We could also have defined the above functions like so:

const toNumbers = of<number>(1); // const toNumbers: number[]
const toStrings = of<string>("Test Of"); // const toString: string[]
Enter fullscreen mode Exit fullscreen mode

Technically we could have used any:

function of(a: any) : any {
  if (a.length !== undefined) {
    return a
  }
  return a;
}
Enter fullscreen mode Exit fullscreen mode

But there is a big difference between using any and generics. If you take a closer look at the above example, we don't know anything about the input argument. Calling of with an undefined or null value will result in an error. Generics can infer the exact type and enforce to handle the input accordingly inside the function body. 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];
}
Enter fullscreen mode Exit fullscreen mode

We have to be more explicit when dealing with generics, the example can be rewritten to the following:

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[]
Enter fullscreen mode Exit fullscreen mode

Using generics enables us to reuse functionality, as a is of type Type or an array of type Type. When passing in 1 as an argument, Type binds to number, the same happens when passing in [1], Type binds to number.

While we have seen functions using generics, we can also use generics with classes, which might be interesting when writing 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[]
Enter fullscreen mode Exit fullscreen mode

The examples we have seen so far should help us in understanding the basics, we will build on this knowledge when using generics with React components.

React and Generics

When working with React we might have a function component where we need to infer the argument type.
We might be building a component that expects a number or string or an array of type number or string.

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!
Enter fullscreen mode Exit fullscreen mode

This works, but it also works for any value right now. We can pass in true and TypeScript will not complain. We need to restrict Type by ensuring Type either extends number or string.

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!
Enter fullscreen mode Exit fullscreen mode

We can ensure that only expected types can be provided now. It's also interesting to note that we can make our prop type definition generic, as seen in the above example:

type RowProps<Type> = {
  input: Type | Type[];
};
Enter fullscreen mode Exit fullscreen mode

Next, we will 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 calculate a third value and the pass in a flat object based on the original inputs as well as the newly calculated value to a provided render prop.

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

type RowComponentPropTypes<InputType, OtherInputType> = {
  input: InputType;
  otherInput: OtherInputType;
  render: (props: RenderPropType<InputType, OtherInputType>) => JSX.Element;
};
Enter fullscreen mode Exit fullscreen mode

The first step is to define the RowComponentPropTypes, where we let TypeScript infer the provided arguments, and based on the bind types define the render function via using RenderPropType. RenderPropType is an intersection of the new type {c: number}, which we will calculate, and InputType and OtherInputType. We have been making heavy use of generics so far.

We might not know the exact shape of the provided inputs, so our next step is to restrict the provided types on the component level.

class RowComponent<
  InputType extends { a: number },
  OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
  // implementation...
}
Enter fullscreen mode Exit fullscreen mode

By using InputType extends { a: number } we can ensure that our input has an a property of type number provided, the same for OtherInputType. Now we can implement the RowComponent that ensures we can provide a, b, c properties to a render function.

Finally, this is our complete example implementation:

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>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

We should have a basic understanding of generics and how to leverage them when working with React and TypeScript now.

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Top comments (2)

Collapse
 
leoyli profile image
Leo Y. Li

Would suggest to change JSX.Element to React.ReactNode since it's less magical and it can be future safe.

Collapse
 
gonzasanchez profile image
Gonzalo Sanchez

This was so useful! Thanks!!