使用参数或泛型来约束另一个参数的类型?
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 并使用 kind
或 template
判别式 属性、 或 您应该将两个参数传递给 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
属性可以用来区分它是哪种类型。幸运的是,您可以使用字符串操作构建模板路径,但如果这对您不起作用,您可以设置查找 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 的相同点也出现在这里。
希望能给您一些想法。祝你好运!
首先,让我说我不确定重构它是否有意义。特别是因为模板是文件路径。对于 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
}
现在,对于函数,我们为 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
你的问题是我们必须在函数中传递模板的路径,这不是很方便(如@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});
希望对您有所帮助。
我想学习如何更有效地使用泛型,因此想尝试重构一段目前冗长且重复的代码。
目前我有这个:
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 并使用 kind
或 template
判别式 属性、 或 您应该将两个参数传递给 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
属性可以用来区分它是哪种类型。幸运的是,您可以使用字符串操作构建模板路径,但如果这对您不起作用,您可以设置查找 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 的相同点也出现在这里。
希望能给您一些想法。祝你好运!
首先,让我说我不确定重构它是否有意义。特别是因为模板是文件路径。对于 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
}
现在,对于函数,我们为 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
你的问题是我们必须在函数中传递模板的路径,这不是很方便(如@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});
希望对您有所帮助。