cassiozen
cassiozen

Reputation: 269

TypeScript template literal string from nested object

I want to get a union of all the keys under states in this object type,

I have a nested object of state keys. I want to get a union of all these keys under state in dot notation.

For example, for this config:

type Config = {
initial: string;
states: {
    idle: {
        on: {
          START: string;
        };
    };
    running: {
        on: {
            PAUSE: string;
        };
    };
    paused: {
        initial: string;
        states: {
            frozen: {
                on: {
                    HEAT: string;
                };
            };
        };
        on: {
          RESET: string;
        };
    };
};

I want 'idle' | 'running' | 'paused' | 'paused.frozen'

Is this possible? Any ideas?

Upvotes: 3

Views: 759

Answers (3)

Oblosys
Oblosys

Reputation: 15096

You can do this with a recursive conditional type like this:

type StateKeys<T> = T extends {states: infer S}
  ? keyof S | StateKeys<S[keyof S]>
  : never

type Test = StateKeys<Config>
// type Test = "idle" | "running" | "paused" | "frozen"

TypeScript playground

Ah, I missed that you needed paused.frozen instead of just frozen. For what it's worth, my old solution could be fixed like this, using just conditional types:

type StateKeysForKey<S, K> = K extends keyof S & string
  ? `${K}.${StateKeys<S[K]>}`
  : never

type StateKeys<T> = T extends {states: infer S}
  ? keyof S | StateKeysForKey<S, keyof S>
  : never

type Test = StateKeys<Config>
// type Test = "idle" | "running" | "paused" | "paused.frozen"

TypeScript playground

Upvotes: 1

jcalz
jcalz

Reputation: 327614

Looks like another job for recursive conditional types as well as template literal types:

type StatesKeys<T> = T extends { states: infer S } ? {
  [K in Extract<keyof S, string>]: K | `${K}.${StatesKeys<S[K]>}`
}[Extract<keyof S, string>] : never

type ConfigStatesKeys = StatesKeys<Config>;
// type ConfigStatesKeys = "idle" | "running" | "paused" | "paused.frozen"

StatesKeys<T> inspects T for its states property S, and generates for each of its keys K the union we want, which is K itself, plus the possible concatenation of K with a dot and StatesKeys<S[K]>>. That is, we are concatenating each key K with any nested keys from S[K]. If there are no nested keys, and StatesKeys<S[K]> is never, the template literal will also become never, so we don't have to special-case it.

Playground link to code

Upvotes: 6

xargr
xargr

Reputation: 3088

You can use keyof keyword, in your particular example one solution could be keyof states

Upvotes: -2

Related Questions