sandrozbinden
sandrozbinden

Reputation: 1607

Typescript compiler infering type with generics

I try to achieve a generic isEventOfType function. In multiple places events from a x-state statemachine are defined (see Maschine1Events, Maschine2Events). When processing events x-states can only provide a Maschine1Events or Maschine2Events but can't tell exactly which event is processed.

I therefore need a method to check if the event is of a type that I expect. For example that the 'CHANGE' event is provided.

This example is fully functional, but what I fail to understand is why inside the fWithoutGeneric function the types can't be infered.

    export type Event<TType, TData = unknown> = { type: TType } & TData;
    export type Value<TType> = { value: TType };

    type Maschine1Events = Event<'INC'> | Event<'DEC'> | Event<'CHANGE', Value<number>>;
    type Maschine2Events = Event<'INC', Value<number> | Event<'SEND_MESSAGE', Value<string>>;

    type EventTypes<T extends Event<unknown>> = T['type'];
    type EventOfType<T extends Event<unknown>, U extends EventTypes<T> = EventTypes<T>> = Extract<T, { type: U }>;
    function isEventOfType<T extends Event<unknown>, U extends EventTypes<T> = EventTypes<T>>(
        event: T,
        type: U
    ): event is EventOfType<T, U> {
        return event.type === type;
    }

    function fWithGenerics(event: Maschine1Events) {
        if (isEventOfType<Maschine1Events, 'CHANGE'>(event, 'CHANGE')) {
            const a: Event<'CHANGE', Value<number>> = event;
        }
    }
    function fWithOutGenerics(event: Maschine2Events) {
        if (isEventOfType(event, 'SEND_MESSAGE')) {
            const a: Event<'SEND_MESSAGE', Value<string>> = event; //Why can't this be infered
        }
    }

Playground link to code

Upvotes: 0

Views: 70

Answers (1)

jcalz
jcalz

Reputation: 330411

I'd be inclined to give isEventOfType() the following signature:

function isEventOfType<E extends { type: string }, T extends E['type']>(
    event: E,
    type: T
): event is Extract<E, { type: T }> {
    return event.type === type;
}

I simplified the parameters a bit and removed the generic parameter defaults for the second parameter. I don't know what the point was of those, but unless you need them they are probably just a distraction.

The big change here is that the E type of the event parameter is now constrained to {type: string} instead of {type: unknown}. The string gives the compiler a hint to infer a string literal type for the type T of the type parameter (see microsoft/TypeScript#10676 for details about about how inference of literal types happens).

Now the code works as you expect, I think:

function m1(event: Maschine1Events) {
    if (isEventOfType(event, 'CHANGE')) {
        event.value.toFixed(2); // okay
    } else {
        event // {type: "INC"} | {type: "DEC"}
    }
}
function m2(event: Maschine2Events) {
    if (isEventOfType(event, 'SEND_MESSAGE')) {
        event.value.toUpperCase(); // okay
    } else {
        event.value.toFixed(2); // okay
    }
}

Playground link to code

Upvotes: 4

Related Questions