3

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?

3
  • Do you want the values or a union type? And for this specific structure or for any general object with arbitrary nesting? For just this thing you could do type KeyType = keyof Config.states | keyof Config.states.paused.states; although that doesn't generalize. Commented Mar 26, 2021 at 15:37
  • Yeah, the config object might change, so I need a generic solution for nested states Commented Mar 26, 2021 at 15:49
  • I doubt you could do that as I'm not sure the Typescript type language supports that sort of recursion but I'd be fascinated to be proven wrong! EDIT - I was! Commented Mar 26, 2021 at 15:51

3 Answers 3

6

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

Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

-2

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

2 Comments

What about the nested states (e.g. frozen)?
Yeah, that won't cover the nested states

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.