我想学习如何更有效地使用泛型,因此想尝试重构当前冗长且重复的代码。
目前我有这个:
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
的形状。但是我对类型太陌生了,无法理解我的要求或实际操作方法。
我的问题是:这将如何重构?如果听起来我从根本上讲是错误的树,我也欢迎广泛的反馈,您的想法会受到赞赏。
答案 0 :(得分:3)
您应该将FooData | SpamData
转换为具有kind
或template
判别属性的discriminated union,或您应该将两个参数传递给renderTemplate
,第一个类似kind
或template
字符串。无论哪种情况,都应选择一些字符串文字来区分数据类型。我将在此处使用"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!
您会看到Data
是FooData
和SpamData
的并集,它们每个都有一个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的观点也与此相同。
希望能给您一些想法。祝你好运!
答案 1 :(得分:1)
首先,我要说的是我不确定重构它是否有意义。特别是因为模板是文件路径。对于TypeScript ./foo.template
和foo.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
或者,我们可以利用泛型和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
答案 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});
希望它会有所帮助。