To accomplish this, you can utilize various TypeScript features such as generics, keyof type operator, indexed access types, conditional types, mapped types, template literal types and utility types.
Start by creating a discriminated union map:
type DiscriminatedUnionMap<Union extends object, Discriminant extends keyof Union> = {
[
DiscriminantType in Union[Discriminant] extends keyof any
? Union[Discriminant]
: never
]: Omit<
Extract<Union, { [_ in Discriminant]: DiscriminantType }>,
Discriminant
>;
};
/*
* Equivalent to:
*
* type ShapeMap = {
* triangle: {
* base: number;
* height: number;
* };
* rectangle: {
* width: number;
* height: number;
* };
* };
*/
type ShapeMap = DiscriminatedUnionMap<Shape, "type">;
Next, transform the map and extract its values as a type union:
/*
* Equivalent to:
*
* type ShapeMessage =
* | {
* contentType: "application/vnd.shapes.triangle";
* content: {
* base: number;
* height: number;
* };
* }
* | {
* contentType: "application/vnd.shapes.rectangle";
* content: {
* width: number;
* height: number;
* };
* };
*/
type ShapeMessage = {
[P in keyof ShapeMap]: {
contentType: `application/vnd.shapes.${P}`;
content: ShapeMap[P];
};
}[keyof ShapeMap];
Here's an example of how you can use it:
function processShapeMessage(message: ShapeMessage) {
switch (message.contentType) {
case "application/vnd.shapes.triangle": {
const { base, height } = message.content;
console.log("Triangle base and height are:", base, "and", height, "respectively");
break;
}
case "application/vnd.shapes.rectangle": {
const { width, height } = message.content;
console.log("Rectangle width and height are:", width, "and", height, "respectively");
break;
}
}
}
Please note: The DiscriminatedUnionMap
is reusable for any discriminated unions with a non-nested discriminant field.