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

Use an argument or generic to constrain type of another argument?

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

目前我有这个:

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 的形状.但是我对类型太陌生了,无法理解我在问什么,也不知道该怎么做。

我的问题是:如何重构它?如果听起来我从根本上说错了树,我也愿意接受广泛的反馈,感谢您的想法。

您应该将 FooData | SpamData 变成 discriminated union 并使用 kindtemplate 判别式 属性、 您应该将两个参数传递给 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属性可以用来区分它是哪种类型。幸运的是,您可以使用字符串操作构建模板路径,但如果这对您不起作用,您可以设置查找 table.

双参数方法如下所示:

  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

首先,让我说我不确定重构它是否有意义。特别是因为模板是文件路径。对于 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
}

现在,对于函数,我们为 template 参数添加一个通用参数 T,该参数被限制为 属性 来自 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

你的问题是我们必须在函数中传递模板的路径,这不是很方便(如@lukasgeiter 所示)。

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

  • discriminated union 是一个性感的模式,但并不总是可用。在这种情况下它是一种数据类型,但假设该数据来自服务器,kind discrimate 属性 可能不存在;

  • 字符串操作不安全,我建议使用映射{ [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});

希望对您有所帮助。