This write up is based on Matt Pocock’s TypeScript tip thread.
Challenge
Say, we have a union type like the following:
type EntityPlain = { type: 'user' } | { type: 'post' };
And we’d like to transform this Entity
type into some other union. This transformation could be the addition of a new property to each of the union members.
Say, we wanted to have a id property on each of the union members that includes the name of the entity type. In such a case, the desired type would look like so:
type EntityWithId = { type: 'user'; userId: string } | { type: 'post'; postId: string };
The caveat is that we’d like to generate this new using the type properties already defined, rather than typing it out manually as above.
Solution
We can leverage a similar pattern as used in deriving a union from an object, and leverage the following TS features:
- mapped types to iterate over sub-types in the original type to use in the new type
- string literal types to concatenate the id to the type and form a type + id property
- the record utility type to map a property (’type’) of our existing entity type to a new type (’typeId’)
- indexed access type to form the final union in the desired shape
This is how it would be achieved:
type EntityWithId = {
[EntityType in Entity['type']]: {
[K in EntityType]: {
type: EntityType;
} & Record<`${EntityType}Id`, string>;
};
}[Entity['type']];
Breakdown
It helps to break the solution into two steps to observe what’s going on:
- Recreate the original type in a more flexible form
- Add the new dynamic property
Step 1:
type EntityWithId = {
// iterate over all entity types (user, post)
// create an object type and collect them into a union
[EntityType in Entity['type']]: {
// iterate over entity types again and create
// a nested object type
[K in EntityType]: {
type: EntityType; // { type: user } | { type: post }
};
};
}[Entity['type']]; // use indexed access type to create a union
Step 2:
Now that we have the new EntityWithId type in a more extensible form, we can do the following:
type EntityWithId = {
[EntityType in Entity['type']]: {
[K in EntityType]: {
type: EntityType;
} & Record<`${EntityType}Id`, string>;
};
};
Here, we add the new property by using the Record<Keys, Type> utility type and leverage template literal type to inject theEntityType
(post/user) into the property name.