Home > Software design >  How to create a union of a list of types from a mapped type?
How to create a union of a list of types from a mapped type?

Time:12-07

I have a notification system in my app, which I want to make type safe. So I defined a type holding the possible notification IDs:

export interface IRequisitionCallBackMethods {
    "open": (requestId: string, values: { type: string; name: string }) => Promise<void>;
    "close": (requestId: string, values: { name: string }) => Promise<void>;
    "delete": (requestId: string, values: { id: number }) => Promise<void>;
}

In my class I can now use a type mapping to define a registration function for callbacks, triggered when one of the events are sent:

export class RequisitionHub {

    public register = <K extends keyof IRequisitionCallBackMethods>(event: K,
        callback: IRequisitionCallBackMethods[K]): void => {
        // todo
    };
}

That works so far, but only for a single event callback combination. In my old implementation I can register the same callback for many events. That's why I need a way to build a type union out of the possible callback types. Is that possible and how?

I tried:

export class RequisitionHub {
    public register2 = <K extends (keyof IRequisitionCallBackMethods | Array<keyof IRequisitionCallBackMethods>)>
    (event: K,
        callback: <union of IRequisitionCallBackMethods valid for K>): void => {
        // todo
    };
}

but didn't find anything that would work for the callback union type.

For comparison, that's what I currently use:

export type RequisitionCallback = (requestId: string, args?: unknown[]) => Promise<void>;

CodePudding user response:

You have a couple of options here. If we start with your IRequisitionCallBackMethods type, we can get the types of the individual functions and — more usefully — their values types:

export interface IRequisitionCallBackMethods {
    "open": (requestId: string, values: { type: string; name: string }) => Promise<void>;
    "close": (requestId: string, values: { name: string }) => Promise<void>;
    "delete": (requestId: string, values: { id: number }) => Promise<void>;
}

export type IRequisitionOpenCallback = IRequisitionCallBackMethods["open"];
export type IRequisitionOpenValues = Parameters<IRequisitionOpenCallback>[1];

export type IRequisitionCloseCallback = IRequisitionCallBackMethods["close"];
export type IRequisitionCloseValues = Parameters<IRequisitionCloseCallback>[1];

export type IRequisitionDeleteCallback = IRequisitionCallBackMethods["delete"];
export type IRequisitionDeleteValues = Parameters<IRequisitionDeleteCallback>[1];

Or alternatively, you could start with those and then define IRequisitionCallBackMethods using those types.

export type IRequisitionOpenValues = { type: string; name: string };
export type IRequisitionOpenCallback = (requestId: string, values: IRequisitionOpenValues) => Promise<void>;

export type IRequisitionCloseValues = { name: string };
export type IRequisitionCloseCallback = (requestId: string, values: IRequisitionCloseValues) => Promise<void>;

export type IRequisitionDeleteValues = { id: number };
export type IRequisitionDeleteCallback = (requestId: string, values: IRequisitionDeleteValues) => Promise<void>;

export interface IRequisitionCallBackMethods {
    "open": IRequisitionOpenCallback;
    "close": IRequisitionCloseCallback;
    "delete": IRequisitionDeleteCallback;
}

Either way, you now have types you can readily use to declare a handler for multiple events:

const handler = async (requestId: string, values: IRequisitionOpenValues | IRequisitionCloseValues) => {
    console.log(requestId);
    if ("type" in values) {
        console.log(values.type, values.name);
    } else {
        console.log(values.name);
    }
};

const r = new RequisitionHub();
r.register("open", handler);
r.register("close", handler);

Playground for first approach

Playground for second approach

Or if you prefer, type the function rather than the values, either works:

const handler: IRequisitionOpenCallback & IRequisitionCloseCallback = async (requestId, values) => {
    console.log(requestId);
    if ("type" in values) {
        console.log(values.type, values.name);
    } else {
        console.log(values.name);
    }
};
  • Related