Home > front end >  Restricting typescript string literal values based on interface
Restricting typescript string literal values based on interface

Time:11-17

I'm trying to restrict the possible values of a string based on the keys of an interface. This might not be the best way to, if you know of a better way please let me know.

interface Events {
  "home": {
    "button": "press"
  },
  "contact": {
    "button": "press",
    "button2": "press" | "longpress",
  }
}

type EventName<
  E = Events,
  Context extends Extract<keyof E, string> = Extract<keyof E, string>,
  Object extends Extract<keyof E[Context], string> = Extract<keyof E[Context], string>,
  Action extends string & E[Context][Object] = E[Context][Object],
  > = `${Context}-${Object}-${Action}`


const works: EventName = "home-button-press";
const doesnt: EventName = "home-button2-longpress";
//    ^^^^^^ Error: Type '"home-button2-longpress"' is not assignable to type '"home-button-press" | "contact-button-press"'.

Is seems to only be allowing the intersection of the set of strings in both objects, where as I want to restrict the possible values based on the previous key.

CodePudding user response:

You need mapped types if you your going to crawl through a tree like this. Otherwise the branch types are only allowed to be what ALL branches have in common. A mapped type let's typescript crawl each branch looking at the types there uniquely.


I would divide this into two parts. First, we need a type that turns a single level of key/string pairs into key-string strings.

I think that looks like this:

type KeyValuePairsAsString<
    T extends Record<string, string>,
> = {
    [K in keyof T & string]: `${K}-${T[K]}`
}[keyof T & string]

const testA: KeyValuePairsAsString<Events['contact']> = 'button2-press'
const testB: KeyValuePairsAsString<Events['home']> = 'button2-press' // error

Here testB errors because button2 is not found in home.

Now we need a mapped type. This should flatten the deepest nodes in the tree first into a union of strings, and then you flatten the result of that into the final union of strings.

type EventName = KeyValuePairsAsString<{
    [Context in keyof Events]: KeyValuePairsAsString<Events[Context]>
}>

const works1: EventName = "home-button-press";
const works2: EventName = "contact-button2-longpress";
const doesnt: EventName = "home-button2-longpress"; // error as expected

Playground

  • Related