typescript 如何动态地确保对象中的函数只使用它所接受的参数来调用?

4jb9z9bj  于 2022-12-24  发布在  TypeScript
关注(0)|答案(2)|浏览(115)

请考虑以下内容,

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 {
    template: keyof typeof templates;
    params: any;
}
const generateText = ({ template, params }: IGenerateText) => templates[template](params);

我如何使用params: any重构部件,以便typescript将获得以下内容:

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

yvgpqqbh1#

这似乎起到了作用。我想你也可以用一些聪明的方法去掉函数内部的as any

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
1l5u6lss

1l5u6lss2#

在外部(即调用函数时),有一个mapped type(就像您的例子中的typeof templatestemplates是一个Map/字典)和一个选择Map类型的键的推断泛型类型参数就足够了,以表达一些函数参数之间的相关性,就像@ARX的回答中所做的那样。
但是在内部(即在函数体中),这种Map类型是不够的:TypeScript静态分析无法知道只使用了一个键类型(它仍然可以是键的并集),因此它只能保留所有类型的并集。
告诉TS只有一个键值存在的唯一方法是使用一些type narrowing。要让TS缩小相关参数的类型,它们必须是有区别的联合体的一部分。不幸的是,在您的情况下,这可能会导致一些重复的代码,目前无法避免以保持完全安全:

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链接
在您的例子中,函数内部的作用是显而易见的,我们可以接受类型Assert以避免重复的代码。

相关问题