I have an interface definition as follows
export interface RoutingMap {
ticket: {
id: number
},
car: {
model: string,
make: number
}
}
I want to be able to create an interface for an object, which has 2 properties - 1 being a key in the RoutingMap
and the other being whatever the type of that Key
in the RoutingMap
is.
In pseudo-code I image it looking like this
export interface RoutingMessage {
resource: keyof RoutingMap,
params: RoutingMap["resource"]
}
The end goal is for me to be able to construct objects like this
const msg: RoutingMessage = {
resource: "ticket",
params: { id: 10 }
}
const invalidMsg: RoutingMessage = {
resource: "something", //TS error - "something" is not a key of routing map
params: { id: 10 }
}
const invalidMsg2: RoutingMessage = {
resource: "car"
params: { model: "tesla", make: true } //TS error - "make" is not a boolean
}
const invalidMsg3: RoutingMessage = {
resource: "car"
params: { id: 123 } //TS error - id is not assinable to {model: string, make: boolean}
}
The RoutingMap
is something which would be extended with time, and people should be able to create specific objects, based on the keys (and their values)
As you can see from the pseudo-code example I can set a constraint for the resource, but I need a way to constrain the params property to only allow objects, which match the signature of the Key
in the RoutingMap
CodePudding user response:
Because there are a finite number of keys that match keyof RoutingMap
, you can write RoutingMessage
as a union of object types where each element of the union corresponds to a particular key, like this:
type RoutingMessage = {
resource: "ticket";
params: {
id: number;
};
} | {
resource: "car";
params: {
model: string;
make: number;
};
}
You can compute RoutingMessage
programmatically from RoutingMap
by writing it as a "distributive object type" (terminology borrowed from microsoft/TypeScript#47109) where we make a mapped type where each property with key K
in RoutingMap
is mapped to the desired object type for that key, and then we immediately index into it with keyof RoutingMap
, producing the desired union.
type RoutingMessage = { [K in keyof RoutingMap]:
{ resource: K, params: RoutingMap[K] }
}[keyof RoutingMap];
With the above definition, RoutingMessage
will gain new union members whenever a new property is added to RoutingMap
, as desired.
Let's make sure it behaves how you want:
const msg: RoutingMessage = {
resource: "ticket",
params: { id: 10 }
}; // okay
const invalidMsg: RoutingMessage = {
resource: "something", // error!
//~~~~~~ <-- Type '"something"' is not assignable to type '"ticket" | "car"'
params: { id: 10 }
};
const invalidMsg2: RoutingMessage = {
resource: "car",
params: { model: "tesla", make: true } // error!
// --------------------> ~~~~
// Type 'boolean' is not assignable to type 'number'.
};
const invalidMsg3: RoutingMessage = {
resource: "car",
params: { id: 123 } // error!
// -----> ~~~~~~~
// Type '{ id: number; }' is not assignable to type '{ model: string; make: number; }'
};
Looks good!
CodePudding user response:
export interface RoutingMessage {
resource: keyof RoutingMap,
params: RoutingMap[this["resource"]]
}