Search code examples
typescripttype-inferencemapped-types

typescript how do i infer from mapping type


Hi I create a plugin and a function to callPlugin action.


export class Plugin {
    actions = {
        'join': ({ groupId }: { groupId: string }) => {

        },
        'addPoint': ({ point }: { point: number }) => {

        }
    }
}


export type PluginParams<T> = T extends {
    actions: Record<infer X, (param: infer Params) => any>
} ? {
    action: X,
    data: Params, // how do i do mapping type  ?
} : never;

function callPlugin<T>(v: PluginParams<T>) {
    console.log(v);
}


callPlugin<Plugin>({
    action: 'addPoint',
    data: {
        // it has an error here how do i fix that 
        // when action is `addPoint` it should only require point.
        point: 100
    }
})


I can infer the params from the plugin, but I don't know how to use them with the mapping type. I simply want it to map action name with the parameters from the action function

I already check all similar question on stackoverflow but i can't still find a solution.

Thank you.

TS Playground


Solution

  • Since you're going to manually specify the generic type argument when you call callPlugin(), like callPlugin<Plugin>({⋯}), then there you want to define PluginParams<T> so that PluginParams<Plugin> becomes a discriminated union of the acceptable inputs of the form:

    type Demo = PluginParams<Plugin>
    /* type Demo = {
        action: "join";
        data: {
            groupId: string;
        };
    } | {
        action: "addPoint";
        data: {
            point: number;
        };
    } */
    

    Here's one way to do that:

    type PluginParams<T extends {
        actions: Record<keyof T["actions"], (p: any) => any>
    }> = { [K in keyof T["actions"]]:
        { action: K, data: T["actions"][K] extends (p: infer P) => any ? P : never }
    }[keyof T["actions"]]
    

    I'm constraining T so that it has an actions property of type Record<keyof T["actions"], (p: any) => any>, so that it only accepts things suitably Plugin-like. Note that instead of the recursive keyof T["actions"] you could probably use string, but sometimes that doesn't play nicely with interface types (see Subtype of Record<string, X> without the index signature).

    Then all we do is map over the properties of T["actions"] and transform them to object with an action and data property of the appropriate type, and subsequently index into it with the union of keys to get a union out. This sort of indexed-mapped-type of the form {[P in K]: F<P>}[K] so that a union in K (e.g., K1 | K2 | K3) produces a union in the output (e.g., F<K1> | F<K2> | F<K3>) is called a "distributive object type" and is described in microsoft/TypeScript#47109.

    You can verify that PluginParams<Plugin> becomes the desired discriminated union, and that your call works as desired if you define callPlugin to accept PluginParams<T>:

    function callPlugin<T extends {
        actions: Record<keyof T["actions"], (p: any) => any>
    }>(v: PluginParams<T>) {
        console.log(v);
    }
    
    callPlugin<Plugin>({
        action: 'addPoint',
        data: {
            point: 100
        }
    })
    
    callPlugin<Plugin>({
        action: 'join',
        data: {
            point: 100 // error! 
            // Type '{ point: number; }' is not 
            // assignable to type '{ groupId: string; }'.
        }
    })
    
    callPlugin<Plugin>({
        action: 'join',
        data: {
            point: 100 // error! 
            // Type '{ point: number; }' is not 
            // assignable to type '{ groupId: string; }'.
        }
    })
    

    Do note that this is a somewhat nonstandard way of writing and calling generic functions. It requires that the caller manually specify the type argument, which is usually inferred. I'd expect that either your callPlugin() function should also take a plugin argument from which T can be inferred, or you should refactor so that all the generic stuff is dispensed with before anyone calls callPlugin(), so that nobody has to remember to specify the type argument manually. This is technically out of scope for the question as asked, so I won't delve further into this, other than to say to be careful that your code can be properly consumed by callers.

    Playground link to code