I’m trying to extract the ACTION_ keys from the following config shape (it’s a state machine). Notice that a state can have initial, states, then any number of actions:
export const config = {
initial: 'STATE_1',
states: {
STATE_1: {
ACTION_5: 'STATE_2',
initial: 'STATE_3',
states: {
STATE_3: {
ACTION_1: 'STATE_4',
ACTION_2: 'STATE_2',
},
STATE_4: {
ACTION_3: 'STATE_3',
},
},
},
STATE_2: {
ACTION_4: 'STATE_1',
ACTION_5: 'STATE_3',
initial: 'STATE_5',
states: {
STATE_5: {
ACTION_6: 'STATE_6',
},
STATE_6: {
ACTION_6: 'STATE_5',
},
},
},
},
};
The best I can come up with at the moment is this:
type ActionUnion<T> = T extends { [K in keyof T]: infer U }
? U extends { states: any }
? ActionUnion<U['states']> // recursive call for nested states
: U extends { [key: string]: any }
? keyof Omit<U, 'initial' | 'states'> // extract action keys
: never
: never;
But this is inferred as "STATE_1" | "STATE_2".
type T1 = ActionUnion<typeof config>; // should be "ACTION_1" | "ACTION_2" | etc...
I think I’m probably making this more complex than it needs to be.
>Solution :
Utilities:
Prettify – simplifies the type shown by the IDE:
type Prettify<T> = T extends infer R ? { [K in keyof R]: R[K] } : never;
Next, using the mapped types we will map through the object and check if the key of it starts with ACTION_ then we pick the key, otherwise if the property is an object, we recursively check in the same manner the keys of that property, if none of the previous conditions are true, we just assign never. Then we extract the values of that object to get the union of values:
type ActionUnion<T extends object> = Prettify<
{
[K in keyof T]: K extends `ACTION_${string}`
? K
: T[K] extends object
? ActionUnion<T[K]>
: never;
}[keyof T]
>;
Usage:
// type Actions = "ACTION_4" | "ACTION_5" | "ACTION_6" | "ACTION_3" | "ACTION_1" | "ACTION_2"
type Actions = ActionUnion<typeof config>;