0

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.

1
  • Is "home-button2-longpress" supposed to work? Because button2` does does exist in the home entry. Commented Nov 16, 2021 at 18:25

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.