使用参数还是泛型来约束另一个参数的类型?

时间:2019-09-04 19:05:09

标签: typescript type-inference typescript-generics

我想学习如何更有效地使用泛型,因此想尝试重构当前冗长且重复的代码。

目前我有这个:

interface FooData {
  foo: string;
}

function renderFoo (data: FooData): string {
  return templateEngine.render("./foo.template", data)
}

interface SpamData {
  spam: number;
}

function renderSpam (data: SpamData): string {
  return templateEngine.render("./spam.template", data)
}

templateEngine.render的调用天真地结合了模板路径和数据而无需类型检查,我希望在此基础上建立类型安全性。

上面的代码可以正常工作并确保例如spam.template仅使用类型为SpamData的数据呈现,但是该结构是冗长且重复的。

我认为可能存在一种解决方案,该解决方案提供了一个调用函数(例如renderTemplate),该函数具有一个签名(以某种方式?)基于所选模板来强制data的形状。但是我对类型太陌生了,无法理解我的要求或实际操作方法。

我的问题是:这将如何重构?如果听起来我从根本上讲是错误的树,我也欢迎广泛的反馈,您的想法会受到赞赏。

3 个答案:

答案 0 :(得分:3)

您应该将FooData | SpamData转换为具有kindtemplate判别属性的discriminated union您应该将两个参数传递给renderTemplate,第一个类似kindtemplate字符串。无论哪种情况,都应选择一些字符串文字来区分数据类型。我将在此处使用"foo""spam"。首先,一个有区别的工会:

  interface FooData {
    kind: "foo";
    foo: string;
  }

  interface SpamData {
    kind: "spam";
    spam: number;
  }

  type Data = FooData | SpamData;

  function render(data: Data): string {
    return templateEngine.render("./" + data.kind + ".template", data);
  }

  render({ kind: "foo", foo: "hey" }); // okay
  render({ kind: "spam", spam: 123 }); // okay
  render({ kind: "foo", spam: 999 }); // error!

您会看到DataFooDataSpamData的并集,它们每个都有一个kind属性,可用于区分其类型。可以通过字符串操作来构建模板路径是很幸运的,但是如果这样对您不起作用,则可以设置一个查找表。

两个参数的方法看起来像这样:

  interface FooData {
    foo: string;
  }

  interface SpamData {
    spam: number;
  }

  interface DataMap {
    foo: FooData;
    spam: SpamData;
  }

  function render<K extends keyof DataMap>(kind: K, data: DataMap[K]): string {
    return templateEngine.render("./" + kind + ".template", data);
  }

  render("foo", { foo: "hey" }); // okay
  render("spam", { spam: 123 }); // okay
  render("foo", { spam: 999 }); // error!

在这里,我们提出了一个名为DataMap的映射接口,它表示kind字符串和数据类型之间的关系。尽管我使用了generic函数来捕获render()的参数之间的约束,但它与有区别的联合相似。实际调用templateEngine.render()的关于string-manipulation-vs-lookup的观点也与此相同。


希望能给您一些想法。祝你好运!

Link to code

答案 1 :(得分:1)

首先,我要说的是我不确定重构它是否有意义。特别是因为模板是文件路径。对于TypeScript ./foo.templatefoo.template不同,而对于模板引擎,它们可能是同一件事。但是,我将让您决定要重构还是保持原样。

这是我针对此问题的两种解决方案:

函数重载

Function overloads允许您指定替代方法签名,在其中我们可以指定模板和数据接口的组合:

function renderTemplate(template: './foo.template', data: FooData): string;
function renderTemplate(template: './spam.template', data: SpamData): string;
function renderTemplate(template: string, data: any): string {
  return templateEngine.render(template, data);
}

renderTemplate("./unknown.template", {}); // error
renderTemplate("./foo.template", { spam: 42 }); // error
renderTemplate("./foo.template", { foo: 'bar' }); // no error

Playground

泛型

或者,我们可以利用泛型和lookup types来实现相同目的。与函数重载相比,它读起来有点麻烦,但冗长得多。

首先,我们需要在模板名称和数据接口之间进行某种映射。为此,我们将使用一个新的界面:

interface TemplateMap {
  "./foo.template": FooData,
  "./spam.template": SpamData
}

现在,对于该函数,我们为T参数添加了一个通用参数template,该参数被约束为TemplateMap的属性名称。我们通过指定T extends keyof TemplateMap来实现。最后,data参数需要与TemplateMap中的相应类型匹配。我们将使用TemplateMap[T]检索此类型。

function renderTemplate<T extends keyof TemplateMap>(template: T, data: TemplateMap[T]): string {
  return templateEngine.render(template, data);
}

renderTemplate("./unknown.template", {}); // error
renderTemplate("./foo.template", { spam: 42 }); // error
renderTemplate("./foo.template", { foo: 'bar' }); // no error

Playground

答案 2 :(得分:1)

您遇到的困难是我们必须在函数中传递模板的路径,这并不真正方便(如@lukasgeiter所示)。

@jcalz提出了两个好的解决方案,但要注意以下几点:

  • 有区别的联合是一种性感的模式,但并不总是可用的。在这种情况下,它是数据类型,但是假设此数据来自服务器,则kind区分属性可能不存在;

  • 字符串操作不安全,我建议使用映射{ [K in Kind]: TemplatePath; }。根据定义,字符串连接是不安全的,使用非常规路径可能会出错,并且调试时间可能会更长。通过使用映射,您可以将可能的错误源集中到一个常量,该常量更易于维护。

我的代码建议:

interface FooData {
    foo: string;
}

interface SpamData {
    spam: number;
}

interface TemplateMap {
    foo: FooData;
    spam: SpamData;
}

type Kind = keyof TemplateMap;

const templateKindMap: Readonly<{ [K in Kind]: string }> = {
    foo: './foo.template',
    spam: './spam.template'
};

function render<K extends Kind>(kind: K, data: TemplateMap[K]): string {
    return templateEngine.render(templateKindMap[kind], data);
}

render('foo', {foo: ''});
render('spam', {spam: 0});

希望它会有所帮助。