我不得不处理从许多可能的值中获取一个值的问题,但是在这些值中有一个id可以用来获取正确的change
函数,但是对于typescript,这不起作用,因为所有函数的并集是不可调用的。
type Module<T> = {
change: (value: T) => void
}
enum ModuleID {
A = "id:A",
B = "id:B"
}
interface Identifiable<ID extends ModuleID> {
id: ID
}
// Module A
interface A extends Identifiable<ModuleID.A> {
item: 'value:A'
}
export const moduleA: Module<A> = {
change(a: A) {}
}
// Module B
interface B extends Identifiable<ModuleID.B> {
item: 'value:B'
}
export const moduleB: Module<B> = {
change(a: B) {}
}
// Map id to module containing the correct `change` function
const moduleMap = {
[ModuleID.A]: moduleA,
[ModuleID.B]: moduleB
}
// While conceptually correct, this does not work with typescript
function changeValue(value: A | B) {
const module = moduleMap[value.id]
// The intersection 'A & B' was reduced to 'never' because
// property 'item' has conflicting types in some constituents.
module.change(value)
}
我该如何重新构造它,以便可以基于值的id
以类型安全的方式调用适当的change
方法。
1条答案
按热度按时间zwghvu4y1#
这个问题我称之为 * correlated unions *,如microsoft/TypeScript#30581中所述。在
changeValue()
的主体中,module.change
的类型和value
的类型都是联合类型,但是编译器无法对它们之间的 * correlation * 进行建模。如果编译器能够将value
先缩小到A
,然后缩小到B
,它将很高兴:但是它只能通过不同的控制流来实现,这不是你想要的,因为它是冗余的,并且与输入联合体一起冗余地扩展,它不能查看一行代码,并同时为每一个收缩计算它。
microsoft/TypeScript#47109中介绍了处理相关联合的推荐解决方案,它涉及到重构类型,以便使用generics而不是联合,并且泛型操作是根据mapped types和indexes into这类类型显式编写的。目标是
changeValue()
在某些类似键的类型K
中应该是泛型的。使得对于某些F
,value
被视为F<K>
类型,而对于相同的F
,module.change
被视为(value: F<K>) => void
类型。这里有一种方法,首先让我们创建一个类型,在这个类型中我们可以根据
ModuleID
查找change()
的参数类型:然后我们将
moduleMap
的类型显式地写为Map类型:最后,我们将使
changeValue
在K
中泛型化,即所涉及的ModuleID
的类型:那需要一些解释为了使其工作,编译器需要理解
value
是Identifable<K>
,从而value.id
是K
类型,并且还需要理解value
是ChangeArg[K]
类型,从而将其视为moduleMap[value.id]
的有效输入。因此我通过将value
的类型定义为这两种类型的交集来帮助编译器(即使对于每个K
,ChangeArg[K]
已经是Identifiable<K>
的子类型),然后在获取id
属性之前扩展到Identifiable<K>
(因此编译器将其保持为K
,而不会扩展到更不可用的内容)。您可以验证
module.change
现在被视为(value: ChangeArg[K]) => void
类型,它接受Identifiable<K> & ChangeArg[K]
类型的参数,因此changeValue()
的主体编译没有错误。我们还要确保来电者仍然有合理的行为:
看起来不错!
最后,这种重构方式可能不值得你花费时间/精力/复杂度,你完全可以保持代码原样,只使用类型Assert来抑制警告,只要你确信它是正确的:
采用哪种方式取决于哪个对您的用例更重要:如果你想要编译器验证的类型安全,并且不关心复杂性,那么重构可能是一条可行之路;如果你想要编译器验证的类型安全,但又不想太复杂,你可能想通过冗余的控制流来进行收缩。如果你不太关心编译器验证的类型安全,而不想向前发展,那么Assert可能是你的解决方案。这取决于你。
Playground代码链接