FrogTheFrog
FrogTheFrog

Reputation: 1671

Infering correct string literal in method

I need help convincing Typescript to infer string literal which was put it by user, instead of using one with all possible values.

Take a look at this example (playground link):

// Supported methods.
type Methods = "GET" | "PUT" /* and etc. */;

// Event interface for each method.
interface Events {
    [key: string]: [any, any];
}

// Event objects assigned to methods.
type MethodicalEvents = {
    [key in Methods]: Events | undefined;
};

// Extract string keys only.
type EventKeys<E extends Events> = Extract<keyof E, string>;

// Extract all event keys from `MethodicalEvents -> Events`.
type AllEvents<O extends MethodicalEvents, M extends Methods> =
    O[M] extends object ? EventKeys<O[M]> : never;

// Extract send value (first array value in `Events` interface).
type ExtractSendValue<O extends MethodicalEvents, M extends Methods, E extends AllEvents<O, M>> =
    O[M] extends object ?
    O[M][E] extends [any, any] ?
    O[M][E][0] :
    never :
    never;

// Interface implementing stuff from above.
interface Foo extends MethodicalEvents {
    PUT: {
        "1": [123, void];
        "2": [string, void];
        "3": [boolean, void];
    };
}

// Class for making requests via `Foo` interface.
class Bar<O extends MethodicalEvents> {
    public request<M extends Methods, E extends AllEvents<O, M>>(
        method: M,
        event: E,
        data: ExtractSendValue<O, M, E>,
    ) {
        // Do stuff...
    }
}

const bar = new Bar<Foo>();
// `true` should not be allowed.
bar.request("PUT", "1", true /*type: `string | boolean | 123`*/);

// type is `123`, as expected
type ExpectedType = ExtractSendValue<Foo, "PUT", "1">;

Second argument of bar.request has type of "1" | "2" | "3" whereas I would like it to have "1" as a type.

How can I achieve this?

Upvotes: 1

Views: 36

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

I can't tell you for sure why the inference does not work as you expect it to, I would expect it to. AllEvents<O, M> should work out to a constraint of "1"|"2"|"3", which should in turn let the compiler infer for E the literal type "1". It instead infers "1"|"2"|"3".

From my testing the issue is with the use of Extract in Extract<keyof E, string>; If we remove this the inference works as expected (although this might be required for your use-case).

A workaround for the bug is reordering the conditionals in AllEvent. This seems to work:

// Supported methods.
type Methods = "GET" | "PUT" /* and etc. */;

// Event interface for each method.
interface Events {
    [key: string]: [any, any];
}

// Event objects assigned to methods.
type MethodicalEvents = {
    [key in Methods]: Events | undefined;
};

// Extract all event keys from `MethodicalEvents -> Events`.
type AllEvents<O extends MethodicalEvents, M extends Methods> = Extract<O[M] extends object ? keyof O[M] : never, string>

// Extract send value (first array value in `Events` interface).
type ExtractSendValue<O extends MethodicalEvents, M extends Methods, E extends AllEvents<O, M>> =
    O[M] extends object ?
    O[M][E] extends [any, any] ?
    O[M][E][0] :
    never :
    never;

// Interface implementing stuff from above.
interface Foo extends MethodicalEvents {
    PUT: {
        "1": [123, void];
        "2": [string, void];
        "3": [boolean, void];
    };
}

// Class for making requests via `Foo` interface.
class Bar<O extends MethodicalEvents> {
    public request<M extends Methods, E extends AllEvents<O, M>>(
        method: M,
        event: E,
        data: ExtractSendValue<O, M, E >,
    ) {
        // Do stuff...
    }
}

const bar = new Bar<Foo>();
// `true` is not allowed
bar.request("PUT", "1", true /*type: `string | boolean | 123`*/);

// type is `123`, as expected
type ExpectedType = ExtractSendValue<Foo, "PUT", "1">;

Playground link

Upvotes: 1

Related Questions