loading...

Notes on TypeScript: React and Generics

busypeoples profile image A. Sharif ・5 min read

Notes on TypeScript (17 Part Series)

1) Notes on TypeScript: Pick, Exclude and Higher Order Components 2) Notes on TypeScript: Render Props 3 ... 15 3) Notes on TypeScript: Accessing Non Exported Component Prop Types 4) Notes on TypeScript: ReturnType 5) Notes on TypeScript: Phantom Types 6) Notes on TypeScript: Type Level Programming Part 1 7) Notes on TypeScript: Conditional Types 8) Notes on TypeScript: Mapped Types and Lookup Types 9) Notes on TypeScript: React and Generics 10) Notes on TypeScript: Fundamentals For Getting Started 11) Notes on TypeScript: Type Level Programming Part 2 12) Notes on TypeScript: Inferring React PropTypes 13) Notes on TypeScript: React Hooks 14) Notes on TypeScript: Recursive Type Aliases and Immutability 15) Notes on TypeScript: Handling Side-Effects 16) Notes on TypeScript: Type Level Programming Part 3 17) Notes on TypeScript: Building a validation library

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;
}

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;
}

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[]

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[]

Technically we could have used any:

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

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];
}

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[]

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[]

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!

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!

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[];
};

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;
};

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

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>
  )}
/>

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

Notes on TypeScript (17 Part Series)

1) Notes on TypeScript: Pick, Exclude and Higher Order Components 2) Notes on TypeScript: Render Props 3 ... 15 3) Notes on TypeScript: Accessing Non Exported Component Prop Types 4) Notes on TypeScript: ReturnType 5) Notes on TypeScript: Phantom Types 6) Notes on TypeScript: Type Level Programming Part 1 7) Notes on TypeScript: Conditional Types 8) Notes on TypeScript: Mapped Types and Lookup Types 9) Notes on TypeScript: React and Generics 10) Notes on TypeScript: Fundamentals For Getting Started 11) Notes on TypeScript: Type Level Programming Part 2 12) Notes on TypeScript: Inferring React PropTypes 13) Notes on TypeScript: React Hooks 14) Notes on TypeScript: Recursive Type Aliases and Immutability 15) Notes on TypeScript: Handling Side-Effects 16) Notes on TypeScript: Type Level Programming Part 3 17) Notes on TypeScript: Building a validation library

Posted on Mar 2 '19 by:

busypeoples profile

A. Sharif

@busypeoples

Focusing on quality. Software Development. Product Management. https://twitter.com/sharifsbeat

Discussion

markdown guide
 

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