Typescript: Create a Union from a Type

Typescript: Create a Union from a Type

How can you transform a Typescript Type into a Union Type? And most important, Why would you want to do that?

A few days ago, I encountered myself refactoring a code from using multiple useState that handled related states into a useReducer.

My rule of thumb is: If you have 3 related useState, you should move that to be a useReducer

The issue came because the state was a vast set of properties, and I wanted to cut back the time of refactoring, so I want to reuse some of the previous code.

So, I decided that I needed to create the actions for this new reducer based on the names of the State, so basically created a union type based on the State type.

So, the use case:

  • I had a type to represent a State
  • I want to create a list of actions based on that State

The goal was to do something like the following snippet.

type State = {
  searchKeyWords: string;
  isSlidingPanelOpen: boolean;
  selectedJobDocumentId: number;
}

// I want to create this

type Actions = {
  type: 'searchKeyWords',
  payload: string
} | {
  type: 'isSlidingPanelOpen',
  payload: boolean
} | {
  type: 'selectedJobDocumentId',
  payload: number
}

/// So it can be used like this
function reducer(state: State, action: Actions) {
  switch(action.type){
    case 'searchKeyWords':
      return {
        ...state,
        searchKeyWords: action.payload
      }
    case 'isSlidingPanelOpen':
      return {
          ...state,
          isSlidingPanelOpen: action.payload
        }
    case 'selectedJobDocumentId':
      return {
        ...state,
        selectedJobDocumentId: action.payload
      }
    default:
      return state
  }
}

The idea is to automatically take the State type to create the Actions type.

I needed a utility type that takes in an unknown type and spits out a Union with a specific shape to achieve this behavior.

So, a clear use case for Generics and Mapped Types.

🤔 Why? Because generics are the way to write reusable types.

More on that in this Twitter thread

Mapped Types: The main idea of this is to take the properties of a type T to create a new type by using those >properties in another way

Let's create a new Unionize utility type that will take a generic named T that extends an object.

The extends keyword here acts as a way to restrict the type of T to be like an object

This type will iterate over the keys of T and map those to a new shape where each key of T will hold a new object shape.

/**
 * Create a Union type from an Object type
 * that will use the Object key as `type` entry and the
 * Object value as `payload`
 */
type Unionize<T extends object> = {
    [k in keyof T]: { type: k; payload: T[k] };
}[keyof T];

Let's use it!!

type State = {
  searchKeyWords: string;
  isSlidingPanelOpen: boolean;
  selectedJobDocumentId: number;
}

/**
 * Create an Union type from an Object type
 * that will use the Object key as `type` entry and the
 * Object value as `payload`
 */
type Unionize<T extends object> = {
    [k in keyof T]: { type: k; payload: T[k] };
}[keyof T];

// I want to create this

type Actions = Unionize<State>

/// So it can be used like this
function reducer(state: State, action: Actions) {
  switch(action.type){
    case 'searchKeyWords':
      return {
        ...state,
        searchKeyWords: action.payload
      }
    case 'isSlidingPanelOpen':
      return {
          ...state,
          isSlidingPanelOpen: action.payload
        }
    case 'selectedJobDocumentId':
      return {
        ...state,
        selectedJobDocumentId: action.payload
      }
    default:
      return state
  }
}

Check it out in the typescript playground