Discriminated unions of objects containing union types in typescript

Viewed 90

Please suggest a better title, I am not sure what exactly to ask.

I have a type definition that looks like this:

type Animal =
  | {
    'species': 'cat'
    'action': 'meow'
  }
  | {
    'species': 'cat'
    'action': 'whine'
  }
  | {
    'species': 'dog' | 'tree'
    'action': 'bark'
  };

I want to define a conditional type ActionsFor<S> that results in a narrowed-down type of the given species. For example:

type CatActions = ActionsFor<'cat'> // should be 'meow' | 'whine'
type DogActions = ActionsFor<'dog'> // should be 'bark'
type TreeActions = ActionsFor<'tree'> // should be 'bark'
type BarkActions = ActionsFor<'dog'|'tree'> // should be 'bark'

My current attempt is close but does not work as I want with the unioned species:

type ActionFor<S extends Animal['species']> = Extract<Animal, {species: S}>['action']

Which results in:

type CatActions = ActionsFor<'cat'> // correct - 'meow' | 'whine'
type DogActions = ActionsFor<'dog'> // WRONG - never
type TreeActions = ActionsFor<'tree'> // WRONG - never
type BarkActions = ActionsFor<'dog'|'tree'> // correct - 'bark'

How can I redefine ActionsFor to do what I want?

1 Answers

Figured it out. This seems to work, but if anyone can get it shorter or more elegant I'm all ears!

type ActionFor<S extends Animal['species']> = Exclude<Animal, {species: Exclude<Animal['species'], S>}>['action']
Related