Handle unions in React components in TypeScript

#React Props Union

Next technique is extremely simple and useful.

Let's say you have next React component:


interface Props {
    nameA: string;
    nameB?: string;
}

const Component: React.FC<Props> = (props) => {
     const { nameA, nameB } = props 
     const name = nameB || nameA;

     return <div>Hello World! Name: {name}</div>
}

How to make nameB mandatory if we don't pass nameA?

To make it possible, we can use union type.


interface Props1 {
    nameA: string;
    nameB?: string;
}

interface Props2 {
    nameB: string;
}

type Props = Props1 | Props2

It is looks like it works and you can finish your reading...

Try to get nameA property


const Comp: React.VFC<Props> = (props) => {
  if(props.nameA){} // error
}

Bang! Error! But why???

#Safe way to use unions

This is how TS unions works. To fix it, and make it 99% type safe, we can add typeguards.


import React from 'react';

interface Props1 {
  nameA: string;
  nameB?: string;
}

interface Props2 {
  nameB: string;
}

type Props = Props1 | Props2

const hasProperty = <T extends object>(obj: T, prop: string) => 
    Object.hasOwnProperty.call(obj, prop);

const isA = (props: Props): props is Props1 => 
    hasProperty(props,'nameA');
const isB = (props: Props): props is Props2 => 
    !hasProperty(props,'nameA') && hasProperty(props,'nameB');


const Comp: React.VFC<Props> = (props) => {

  if(isA(props)){
    const y = props; // Props1
  }

   if(isB(props)){
    const y = props; // Props2
  }


  return null
}

  const result1 = <Comp nameB="b" /> // ok
  const result2 = <Comp nameA="a" /> // ok

  // error,  'nameB' is missing in type '{}' but required in type 'Props2'
  const result3 = <Comp />

However, there are alternative ways.

This one, is the most common. You just need to add same non-optional property to bothProps1and Props2. Same technique is used for typing reduxactions


import React from 'react';

interface Props1 {
  type:'1'
  nameA: string;
  nameB?: string;
}

interface Props2 {
  type:'2'
  nameB: string;
}

type Props = Props1 | Props2

const Comp: React.VFC<Props> = (props) => {
  if(props.type==='1'){
    const x = props; // Props1
  }
  if(props.type==='2'){
    const x = props; // Props2
  }  


  return null
}

  const result1 = <Comp nameB="b" type="2"/> // ok
  const result2 = <Comp nameA="a" type="1"/> // ok

  // error,  'nameB' is missing in type '{}' but required in type 'Props2'
  const result3 = <Comp />

#Algebraic data types

Here you have another good example which involves algebraic data types definition/explanation.


// credits https://dev.to/gcanti/functional-design-algebraic-data-types-36kf

const enum Messages {
    Success = 'Success',
    Failure = 'Failure'
}

enum PromiseState {
    Pending = 'Pending',
    Fulfilled = 'Fulfilled',
    Rejected = 'Rejected',
}

/**
 * Let's assume we have React state,
 * which implements all of these enums
 * and one extra valid property
 * 
 * Our state can have exact two variants (states)
 * 
 * So how would you write this state?
 * 
 * The first approach:
 */

/**
 * This is the worst interface we can write for
 * this particular case 
 * @product type from algebraic point of view
 * 
 * @question : How many allowed states here can be?
 * @answer   : boolean(2) x Messages(2) x State(3) = 12
 * 
 * @conclusion : more error prone
 */
interface ReactState {
    valid: boolean;
    error: Messages;
    state: PromiseState;
}

/**
 * @question : Should we allow such kind of state ?
 * @answer   : No!
 */

const thisState: ReactState = {
    valid: true,
    error: Messages.Failure,
    state: PromiseState.Pending,
}

/**
 * Much better way.
 * @question : How many state I should allow?
 * @answer   : 2
 * 
 */

interface Failure {
    valid: false;
    error: Messages.Failure;
    state: PromiseState.Rejected
}

interface Success {
    valid: true;
    error: Messages.Success;
    state: PromiseState.Fulfilled
}

/**
 * @SUM type from @algebraic point of view
 * @UNION type from @TypeScript point of view
 *
 * @question   : How many allowed states can be here?
 * @answer     : 2
 *
 * @conclusion : less error prone
 */
export type ResponseState = Failure | Success;

// Try to change some property
const result0: ResponseState = {
    valid: true,
    error: Messages.Success,
    state: PromiseState.Fulfilled
}

const handleState = (state: ResponseState) => {
    if (state.valid === true) {
        state // Success
    } else {
        state; //Failure
    }
}


/**
 * @summary
 * Please use @product types when your properties are self independent
 * and @sum when they are not
 */

You can also use nextutilfor unionizing


// credits goes to Titian Cernicova-Dragomir

import React from 'react';

interface Props1 {
  nameA: string;
  nameB?: string;
}

interface Props2 {
  nameB: string;
}


type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = 
    T extends any 
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type Props = StrictUnion<Props1 | Props2>

With help of StrictUnion you can create more stricter union types. I have found this utility very useful.

Don't forget, that we have a type util for checking whether type is union or not


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;
  
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
November 18, 2020