Home > Enterprise >  How to dynamically ensure that a function in an object is called only with the params that it accept
How to dynamically ensure that a function in an object is called only with the params that it accept

Time:12-22

Consider the following,

interface ITemplateA {
    name: string;
}
const templateA = ({ name }: ITemplateA) => `Hello, I am ${name}`

interface ITemplateB {
    age: number;
}
const templateB = ({ age }: ITemplateB) => `I am, ${age} years old`;

const templates = {
    templateA,
    templateB
}

interface IGenerateText {
    name?: string;
    age?: number;
    template: keyof typeof templates;
    params: any;
}
const generateText = ({ template, params }: IGenerateText) => templates[template](params);

How can I refactor the part with params: any, so that typescript would pick up the following:

generateText({ template: 'templateA', params: { name: 'michael' } }); // no error
generateText({ template: 'templateA', params: { age: 5 } }); // error
generateText({ template: 'templateB', params: { age: 5 } }); // no error

CodePudding user response:

This seems to do the trick. I think you could get rid of the as any inside the function with some cleverness too...

const templateA = ({ name }: { name: string }) => `Hello, I am ${name}`;

const templateB = ({ age }: { age: number }) => `I am, ${age} years old`;

const templates = {
  templateA,
  templateB,
};
type TemplateName = keyof typeof templates;

interface IGenerateText<T extends TemplateName> {
  template: T;
  params: Parameters<typeof templates[T]>[0];
}

function generateText<T extends TemplateName>({ template, params }: IGenerateText<T>) {
  return (templates[template] as any)(params);
}

generateText({ template: "templateA", params: { name: "michael" } }); // no error
generateText({ template: "templateA", params: { age: 5 } }); // error
generateText({ template: "templateB", params: { age: 5 } }); // no error

CodePudding user response:

Externally (i.e. when calling the function), it is enough to have a mapped type (like typeof templates in your case, templates being a map/dictionary) and an inferred generic type argument selecting a key of the mapped type, to express the correlation between some of the function parameters, as done in @AKX's answer.

But internally (i.e. within the function body), this mapped type is not enough: TypeScript static analysis has no way to know that only 1 key type is used (it can still be a union of keys), hence it falls back to keeping the union of all types.

The only way to tell TS that only 1 key value is present, is to use some type narrowing. And to have TS narrow the types of correlated parameters along with it, they must be part of a discriminated union. Unfortunately, in your case this may lead to some repetitive code, which cannot be avoided currently to keep full safety:

type Template = keyof typeof templates;

// Build a discrimated union from the templates map:
type DiscriminatedUnionTemplates = {
    //^? { template: "templateA"; params: ITemplateA; } | { template: "templateB"; params: ITemplateB; }
    // Use a mapped type:
    [T in Template]: {
        template: T;
        params: Parameters<typeof templates[T]>[0]
    }
}[Template] // Use indexed access to convert the mapped type into a union

const generateText = <T extends DiscriminatedUnionTemplates>({ template, params }: T) => {
    // Narrow the type within the union,
    // based on the `template` discriminant key
    switch (template) {
        case 'templateA': return templates[template](params); // Okay
        case 'templateB': return templates[template](params); // Okay, repetitive but currently only way to keep full safety
    }
};

generateText({ template: 'templateA', params: { name: 'michael' } }); // Okay
generateText({ template: 'templateA', params: { age: 5 } }); // Error: Object literal may only specify known properties, and 'age' does not exist in type 'ITemplateA'.
generateText({ template: 'templateB', params: { age: 5 } }); // Okay

Playground Link

In your case, it is obvious what the function does internally, and we may accept the type assertion to avoid the repetitive code.

  • Related