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