Typescript constrain object property depending on another property

Viewed 24

How would one model some types hierarchy in TypeScript that would enforce a limited amount of values to a key inside an object based on the value of another key in the same object?

Example:
Imagine you are tracking all the possible EntryPoints and Triggers that allow a user to see a certain component. For simplicity let's say the component is a Modal in your webpage.

These are the possible values, but not all combinations of them are possible.

enum EntryPoints {
  HOME,
  DASHBOARD,
  SETTINGS,
}

enum Triggers {
  RED_BUTTON,
  BLUE_BUTTON,
  YELLOW_BUTTON,
  ORANGE_BUTTON,
}

In the HOME page we only have BLUE_BUTTON that trigger the modal.
In the DASHBOARD page we only have YELLOW_BUTTON and ORANGE_BUTTON.
In the SETTINGS page we only have BLUE_BUTTON and RED_BUTTON.

So far I defined a Discriminated Union to model this scenario, like so:

type HomeTriggers = {
    entryPoint: EntryPoints.HOME
    trigger: Triggers.BLUE_BUTTON
}

type DashboardTriggers = {
    entryPoint: EntryPoints.DASHBOARD
    trigger: Triggers.YELLOW_BUTTON | Triggers.ORANGE_BUTTON
}

type SettingsTriggers = {
    entryPoint: EntryPoints.SETTINGS
    trigger: Triggers.BLUE_BUTTON | Triggers.RED_BUTTON
}

type ModalTriggers = HomeTriggers | DashboardTriggers | SettingsTriggers

So when trying to use this in userland code:

function foo(trigger: ModalTriggers): void {}

foo({entryPoint: EntryPoints.HOME, trigger: Triggers.RED_BUTTON}) // --> throws error since RED_BUTTON is 
                                                                  // not present in the Home page

I was expecting a less criptic error or at least a good autocompletion from the IDE showing the possible values for trigger after providing an entryPoint (see image below)

enter image description here

1 Answers

In this variant, errors more friendly:

function foo<K extends EntryPoints>(trigger: K extends EntryPoints.HOME ? {
  entryPoint: K
  trigger: Triggers.BLUE_BUTTON
} : K extends EntryPoints.DASHBOARD ? {
  entryPoint: K
  trigger: Triggers.YELLOW_BUTTON | Triggers.ORANGE_BUTTON
} : K extends EntryPoints.SETTINGS ? {
  entryPoint: K
  trigger: Triggers.BLUE_BUTTON | Triggers.RED_BUTTON
} : Error): void { }

// Error will be: Type 'Triggers.RED_BUTTON' is not assignable to type 
// 'Triggers.YELLOW_BUTTON | Triggers.ORANGE_BUTTON'
foo({ entryPoint: EntryPoints.DASHBOARD, trigger: Triggers.RED_BUTTON })

And you don't need ModalTriggers with HomeTriggers, DashboardTriggers, SettingsTriggers.

But, I think, you can not autocomplete half of enum

Related