Reputation: 1671
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
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">;
Upvotes: 1