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 auseReducer
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 ofT
to be like anobject
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