I'm struggling coming up with a good structure for a Command -> Response generic.
My goal is to have a function accept a command from a list of commands based on a Type or Interface, and use that interface to infer the response type.
So far I've managed to correctly infer the expected data
based on the type
property, but I can't seem to wrap my head around inferring the return type of the generic.
Any pointers towards similar examples are very much welcome! I haven't been able to find much
Code example: TypeScript Playground
type MappedData<E, T extends keyof E> = E[T];
type MappedType<E> = {
[K in keyof E]: {
type: K;
data: MappedData<E, K>;
};
}[keyof E];
interface ServerCommandInterface<T> {
_res?: T;
}
interface TestCommandA extends ServerCommandInterface<{responseA: boolean}> {
param_a: string;
}
interface TestCommandB extends ServerCommandInterface<{responseB: boolean}> {
param_b: string;
}
interface Commands {
command_a: TestCommandA;
command_b: TestCommandB;
}
function execute<C extends MappedType<Commands>>(command: C): typeof command.data._res {
// Logic (HTTP call or similar) that would result in _res type
return null as any;
}
const result = execute({
type: 'command_a',
data: {
param_a: 'param',
}
});
console.log(result.responseA); // I expect this to be a boolean
CodePudding user response:
So I think what you're looking for is something like the following helper type:
interface Commands {
command_a: {
param: { param_a: string },
response: { responseA: boolean }
},
command_b: {
param: { param_b: string },
response: { responseB: boolean }
}
}
Here each property of Commands
has a key corresponding to the type
property of the value passed into execute()
. And the value is an object type with two properties: param
and response
. The param
property corresponds to the data
property of the value passed into execute()
, while the response
property corresponds to the value returned from execute()
. Note that the names param
and response
are completely arbitrary and we could have named them anything we wanted. There is not going to be any value of type Commands
involved here; it's just a helper type to allow us to express the call signature of execute()
in an easy way:
function execute<K extends keyof Commands>(
command: { type: K, data: Commands[K]['param'] }
): Commands[K]['response'] {
return null as any;
}
So the execute()
function is generic in the type parameter K
, constrained to be one of the keys of Commands
. Then the parameter to execute()
has a type
property of type K
, and a data
property of type Commands[K]['param']
. (We're using indexed access types to get the type of the K
-keyed property of Commands
and then get the "param"
-keyed property of that). And the return type is Commands[K]['response']
.
Let's see if it works:
const result = execute({
type: 'command_a',
data: {
param_a: 'param',
}
});
Here the compiler infers "command_a"
for K
, and so the call signature is specified as
/* function execute<"command_a">(command: {
type: "command_a";
data: {
param_a: string;
};
}): {
responseA: boolean;
} */
And thus result
is of type
/* const result: {
responseA: boolean;
} */
as expected:
result.responseA === true // okay