3

Given I have the following Options Type

interface Options {
  parent1: {
    child1: {
      foo: 'bar'
    }
  };
  parent2: {
    child2: {
      baz: 'daz'
    }
  };
}

How can I turn it into a union of all 2nd level child property values, in this case

{ foo: 'bar' } | { baz: 'daz' }

I tried the following

type KnownOptions = Options[keyof Options][keyof Options[keyof Options]];

But that only works if the 2nd level child properties are the same across all top level properties. E.g. it works when I rename child2 to `child1

Playground

Gregor
  • 2,325
  • 17
  • 27

3 Answers3

4

You can use a mapped type to fetch the nested type:

type KnownOptions<T> = T extends {
  [k in keyof T]: {
    [k: string]: infer OptionValue
  }
} ? OptionValue : never

// Good:
const a: KnownOptions<Options> = { foo: 'bar' }
const b: KnownOptions<Options> = { baz: 'daz' }

// Expect errors:
const c: KnownOptions<Options> = { foo: 'bad value' }
const d: KnownOptions<Options> = { badKey: 'bar' }

Playground

This is a generic type that accepts a type that has two levels of keys. The conditional type (noted by the ternary T extends Type ? A : B) says "if the generic type T is an object at least 2 level deeps, then return the type at each of those that second levels. Else, return the never type, because the generic type is invalid.

The infer OptionalValue says "whatever type is here, save that as OptionValue". This is then returned by the conditional type, after multiple type have been saved to it, creating a union of each type.

I have to admit, I'm not entire sure why the first level key needed to be k in keyof T but the second level could just be k: string. But, that was the only way I got it to work properly.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
2

I would write KnownOptions like this:

type KnownOptions = { [K in keyof Options]: Options[K][keyof Options[K]] }[keyof Options];

Here we're mapping each property of Options to a union of its properties, and then uniting all of those unions together. It might make more sense if we write it in terms of ValueOf<T> defined like this:

type ValueOf<T> = T[keyof T];

type KO = ValueOf<{ [K in keyof Options]: ValueOf<Options[K]> }>;

So ValueOf<T> gives you a union of the property value types of an object T (e.g., ValueOf<{a: string, b: number}> is string | number) and so what you're doing is mapping each property of Options with ValueOf and then taking ValueOf the whole thing.

You can verify that each of those definitions evaluates to {foo: "bar";} | {baz: "daz";} as desired.


Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

you can index into the type with the ['propName'] notation to extract the type

type KnownOptions = Options['parent1']['child1'] | Options['parent2']['child2']

Damian Green
  • 6,895
  • 2
  • 31
  • 43