Home > Net >  Union event handler type
Union event handler type

Time:01-03

I'm trying to create a type for an event handler to have a working autocomplete for the data of the events. The events I want to handle are structured like this:

type MyEvent =
  | {
    eventA: {
      foo: number;
      bar: number;
    };
  }
  | {
    eventB: {
      baz: string;
    };
  };

This is what I have for the handler so far:

type UnionKeys<T> = T extends T ? keyof T : never;

type Handler = {
  [K in UnionKeys<MyEvent>]: (data: /* ??? */) => void;
};

const handler: Handler = {
  eventA: (data) => console.log(data.foo), // I would like to have working autocomplete for data.* here
  eventB: (data) => console.log(data.baz),
};

I'm not sure how to construct a type for 'data' in the Handler type to match the event. Any ideas?

CodePudding user response:

If you want to stick with exact structure of MyEvent, consider this example:

type UnionKeys<T> = T extends T ? keyof T : never;

// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type MyEvent = StrictUnion<
    | {
        eventA: {
            foo: number;
            bar: number;
        };
    }
    | {
        eventB: {
            baz: string;
        };
    }>;


type Handler = {
    [K in keyof MyEvent]: (data: NonNullable<MyEvent[K]>) => void;
};

const handler: Handler = {
    eventA: (data) => {
        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

Playground

However, I don't like that I was forced to use NonNullable. It seems to be a hack. It is possible to obtain all keys with help of distributive conditional types:

type MyEvent =
    | {
        eventA: {
            foo: number;
            bar: number;
        };
    }
    | {
        eventB: {
            baz: string;
        };
    };

type Keys<T> = T extends Record<infer Key, infer _> ? Key : never;

type Values<T> = T[keyof T]

type Handler = {
    [K in Keys<MyEvent>]: (data: Values<Extract<MyEvent, Record<K, unknown>>>) => void;
};

const handler: Handler = {
    eventA: (data) => {

        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

Playground

Please keep in mind, that there is an easier approach. I believe it worth creating mapped data structure, just like here:

type MyEvent = {
    eventA: {
        foo: number;
        bar: number;
    },
    eventB: {
        baz: string;
    }
}

type Handler = {
    [K in keyof MyEvent]: (data: MyEvent[K]) => void;
};

const handler: Handler = {
    eventA: (data) => {
        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

You might be interested in this example also

  • Related