Home > Net >  Command and response generics in TypeScript
Command and response generics in TypeScript

Time:10-31

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

Playground link to code

  • Related