Building type utils to derive interfaces from unions of interfaces in TypeScript
In this post, we're going to build small, generic type utils in TypeScript.
To work with a real use case, we're going to transform a non-generic code from my previous post into a generic type utility, that could work with more input types, and thus be more reusable.
type Dispatcher = {
[Message in Action as Message['type']]: Message extends { payload: any }
? (payload: Message['payload']) => void
: () => void;
};
Non-generic code, hard to re-use
There are many ways to achieve this goal. We're going to pick a fancy one, leveraging template literals, and type inference to provide a nice development experience:
type Dispatcher =
ValuesAsCallbacks<UnionToKeyValue<Action, 'type:payload'>>;
Reusable, generic type utilities
Now, let's see how to build that π€
A two-step process
First, let's break down the process of turning one type into another. We'll look into the implementation and more detailed explanation in the next section.
1οΈβ£ Β Union to Key-Value
First, we're turning union into a key-value interface by providing a colon-delimited string literal, defining the property to be used as a key and the property to be used as a value. In case the property is missing on an item from the union, we're going to represent this fact with the never
type:
type Action =
| { type: 'reset' }
| { type: 'setValue'; payload: string }
| { type: 'setSelection'; payload: [number, number] | null }
| { type: 'trimWhitespace'; payload: 'leading'|'trailing'|'both' };
type IntermediateResult = UnionToKeyValue<Action, 'type:payload'>;
β²
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β type IntermediateResult = { β
β reset: never; β
β setValue: string; β
β setSelection: [number, number] | null; β
β trimWhitespace: 'leading'|'trailing'|'both'; β
β } β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Can you already see how that is going to turn into an interface with callbacks?
2οΈβ£ Β Values As Callbacks
Second, we're transforming that key-value interface into key-callback interface, where the callback function in the latter is accepting a parameter of the same type as the value in the former:
type IntermediateResult = {
reset: never;
setValue: string;
setSelection: [number, number] | null;
trimWhitespace: 'leading'|'trailing'|'both';
}
type Dispatcher = ValuesAsCallbacks<IntermediateResult>;
β²
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β type Dispatcher = { β
β reset: () => void; β
β setValue: (payload: string) => void; β
β setSelection: (payload: [number, number] | null) => void; β
β trimWhitespace: (payload: 'leading'|'trailing'|'both') => void; β
β } β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Implementation, and a bit of explanation
Union to Key-Value
Let's take a look at how to implement the first utility type π΅οΈ
type UnionToKeyValue<
T extends { [key: string]: any }, β
K extends string β‘
> = ...
(2) It also requires a second parameter to be a string, or a string literal.
... = K extends `${infer Key}:${infer Value}` β’
? Key extends keyof T β£
? ...
: never
: never;
Given
'type:payload'
provided as a second parameter, we're going to infer that Key is 'type'
and Value is 'payload'
. If we don't find a colon in the second parameter, we fail to pattern-match and we return
never
.(4) Next, with the inferred Key, we double-check it belongs to the key set of the first parameter, T β an interface, or a union of interfaces. If it does not belong there, we return
never
.? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never } β€
...
keyof
there.The use of
as
combined with indexed access A[Key]
also gives a clue about elements of the union being interfaces and not primitive types such as undefined or null.Finally, if
A[Key]
does not exist in the element, we're not going to include it in the output. We're not that strict with the Value, if A[Value]
does not exist, we represent that as never
.Full implementation
type UnionToKeyValue<
T extends { [key: string]: any },
K extends string
> = K extends `${infer Key}:${infer Value}`
? Key extends keyof T
? { [A in T as A[Key]]: Value extends keyof A ? A[Value] : never }
: never
: never;
That's most of the heavy work done!
Values as Callbacks
Going from interfaces to interfaces should be slightly easier, let's take a look.
type ValuesAsCallbacks<T extends { [key: string]: any }> = ... β
... = {
[K in keyof T]: T[K] extends never β‘
? () => void β’
: (payload: T[K]) => void; β£
};
never
.(3) If we detect the value is of the type
never
, we return a type representing a callback not accepting any parameters. (4) Otherwise, we return a type representing a single-parameter callback, where the parameter has the same type as the value in the original interface.
Full implementation
type ValuesAsCallbacks<T extends { [key: string]: any }> = {
[K in keyof T]: T[K] extends never
? () => void
: (payload: T[K]) => void;
};
That was it, the last missing piece in the puzzle of turning the union of interfaces into an interface with callbacks π We made it!
Complete Code
The complete code listing from this post can be found under this link:

Final words
Now that you've explored one approach, I would encourage you to try other approaches, for example, splitting UnionAsKeyValue<T, K>
into two separate utils and see how you find it.
Good luck!