Search code examples
typescripttypestyping

TS Pick type based on nested type's keys


Given a type defined by nested types all containing a key label

type Payloads = {
    EventA: { label: 'Foo', content: object }
    EventB: { label: 'Bar', content: object }
    EventC: { label: 'Baz', content: object }
}

And a type defined as the union of select potential label types

type SelectLabels = 'Foo' | 'Bar'

How can I create a type defined as only the keys of Payloads whose value type has a label key whose value type is a member of SelectLabels, resulting in the following behavior:

function f(x: TypeGoesHere) {}
f('EventA') // OK
f('EventB') // OK
f('EventC') // Error

I've tried the following

type TypeGoesHere = Pick<keyof Payloads, Payloads[keyof Payloads]['label'][SelectLabels]>

but get the following error:

Property 'Bar' does not exist on type '"Foo" | "Bar" | "Baz"'.ts(2339)
Property 'Foo' does not exist on type '"Foo" | "Bar" | "Baz"'.ts(2339)

Solution

  • type Payloads = {
        EventA: { label: 'Foo', content: object }
        EventB: { label: 'Bar', content: object }
        EventC: { label: 'Baz', content: object }
    }
    
    type SelectLabels = 'Foo' | 'Bar'
    
    // similar to Object.entries
    type Entries<T> = { [K in keyof T]: [key: K, value: T[K]] }[keyof T]
    
    type TypeGoesHere = Extract<Entries<Payloads>, [any, { label: SelectLabels }]>[0]
    //   ^?
    //   type TypeGoesHere = "EventA" | "EventB"
    
    type Explain1 = Entries<Payloads>
    //   ^?
    //   ['EventA', {label: 'Foo, ...}] | ['EventB', { label: 'Bar', ...}] | ['EventC', { label: 'Baz', ...}]
    // same as Object.entries for runtime
    
    type Explain2 = Extract<Entries<Payloads>, [any, { label: SelectLabels }]>
    //   ^?
    //   ["EventA", { label: 'Foo', ... }] | ["EventB", { label: 'Bar', ... }]
    // pick tuples where value (second item) matches { label: SelectLabels }
    
    type Explain3 = Explain2[0]
    //   ^?
    //   "EventA" | "EventB"
    // take first element of every tuple