枚举映射附加约束的条件类型
Posted
技术标签:
【中文标题】枚举映射附加约束的条件类型【英文标题】:Conditional type for additional constraint on enum mapping 【发布时间】:2021-09-23 07:36:50 【问题描述】:在我的项目中,我有两个枚举 SourceEnum
和 TargetEnum
。对于这两个枚举,都存在一个函数,该函数使用一些取决于枚举值的参数调用。预期参数的确切类型由SourceParams
和TargetParams
两种类型映射定义。
enum SourceEnum
SOURCE_A = 'SOURCE_A',
SOURCE_B = 'SOURCE_B'
enum TargetEnum
TARGET_A = 'TARGET_A',
TARGET_B = 'TARGET_B',
interface SourceParams
[SourceEnum.SOURCE_A]: paramA: string ;
[SourceEnum.SOURCE_B]: paramB: number ;
interface TargetParams
[TargetEnum.TARGET_A]: paramA: string ;
[TargetEnum.TARGET_B]: paramB: number ;
function sourceFn<S extends SourceEnum>(source: S, params: SourceParams[S]) /* ... */
function targetFn<T extends TargetEnum>(target: T, params: TargetParams[T]) /* ... */
我有一个映射,其中包含一个函数来评估每个源值的目标值,我想要做的是,确保用于调用 sourceFn(x, params)
的 params
对象也适用于调用 targetFn(mapping[x](), params)
.为了实现这一点,我创建了这种类型:
type ConstrainedMapping =
[K in SourceEnum]: <T extends TargetEnum>() => (SourceParams[K] extends TargetParams[T] ? T : never)
;
const mapping: ConstrainedMapping =
[SourceEnum.SOURCE_A]: () => TargetEnum.TARGET_A;
// ...
但是像上面那样定义mapping
会给我以下错误:
Type 'TargetEnum.TARGET_A' is not assignable to type ' paramA: string; extends TargetParams[T] ? T : never'.
我的打字看起来很清楚,所以我真的不明白这里有什么问题。我想打字稿在某些时候无法缩小确切的枚举值。
有没有办法做到这一点?我目前正在使用 Typescript 4.2,但我也在 4.3 和 4.4-beta 上进行了尝试,并且都表现出相同的行为。非常感谢 4.2 中的解决方案,但我也可以在未来版本中提供解决方案。
【问题讨论】:
请提供更多致电targetFn(mapping[x](), params)
和sourceFn(x, params)
的示例。你从哪里得到x
?
x
在这里只是一个SourceEnum
类型的任意值。这两个功能实际上并不太相关。我只是想以一种实际的方式表明SourceParams[typeof x]
应该可以分配给TargetParams[typeof mapping[x]()]
用于任何x: SourceEnum
。
【参考方案1】:
因此,您在这里期望 Typescript 看到 <T extends TargetEnum>(): SourceParams[K] extends TargetParams[T] ? T : never;
,然后将 T
的所有可能值分配到此条件中,并在其为真时创建一个联合。
问题是,Typescript 没有这样做。它将T
视为未知,并且除了将 paramA: string;
替换为SourceParams[K]
之外,不会进一步评估此表达式。您正在寻找的分布类型仅出现在 Distributive Conditional Types 中。所以我们必须重写你的ConstrainedMapping
才能使用。
分布式条件类型是一个条件类型别名,其中没有任何参数受到约束。所以首先,我们需要一个实际的type
声明来表示这个返回值,我们不能把它放在更大的ConstrainedMapping
声明中。 (是的,这很奇怪——如果将类型提取到自己的别名中,其行为会有所不同,这是我对 Typescript 设计的最大问题之一。)就像这样:
type TargetWithMatchingParams<S, T> =
S extends SourceEnum
? T extends TargetEnum
? SourceParams[S] extends TargetParams[T]
? T
: never
: never
: never;
我们无法限制S
或T
,因此我们必须在模板中使用更多条件。 (是的,这也很奇怪;改变这样的行为是相当违反直觉的。)我们也不能在这里简单地硬编码整个TargetEnum
,即使这最终是我们想要的——分布必须是跨越一个不受约束的参数到类型别名。
当我们完成那个后,我们就可以在ConstrainedMapping
中使用它了:
type ConstrainedMapping =
[S in SourceEnum]: () => TargetWithMatchingParams<S, TargetEnum>;
;
请注意,该函数不再是通用的(这是您的问题的一部分),我们通过将 TargetEnum
传递给 TargetWithMatchingParams
来实现您正在寻找的分布。
顺便说一句,如果您的真实案例像您的示例一样是静态的,则从 ConstrainedMapping
的定义中删除 () =>
并使用 mapping[x]
而不是 mapping[x]()
“调用”mapping
会稍微提高性能,并且可以说更容易阅读。
最后,这里有几个限制需要注意。
在 extends SourceEnum
不起作用的泛型变量上调用 mapping
。也就是说,targetFn(mapping[SourceEnum.SOURCE_A](), paramA: 'foo' )
可以工作,但 Typescript 却被这样的东西呛住了:
function bar<S extends SourceEnum>(src: S, params: SourceParams[S])
targetFn(mapping[src](), params);
^^^^^^
// Argument of type 'SourceParams[S]'
// is not assignable to parameter of type
// 'TargetParams[ConstrainedMapping[S]]'.
也就是说,Typescript 不够聪明,无法真正理解SourceParams
和TargetParams
之间的关系,并且无法识别任何有效的SourceParams
值将始终匹配对应的TargetParams
。
在我们接受源联合和源参数联合的情况下,基本上是上述的非泛型版本,Typescript 允许不安全的值。考虑:
function baz(src: SourceEnum, params: SourceParams[SourceEnum])
targetFn(mapping(src), params);
baz(SourceEnum.SOURCE_A, paramB: 42 );
这不会导致任何错误,即使我们有 SOURCE_A
和 paramB
并且这是一个无效的组合。不过,归根结底,这只是对 Typescript 联合工作方式的限制——问题在于 baz
的定义或 SourceParams
/TargetParams
的定义,如果 baz
调用 @ 则完全相同987654358@ 根本没有mapping
进入。
这里的太长,未阅读版本只是确保您使用特定枚举类型而不是整个枚举调用 sourceFn
和 targetFn
。 Typescript 不会跟踪单独变量之间的关系,因此它不会检查未知的 SourceEnum
和未知的 SourceParams[SourceEnum]
是否真的在一起,并且没有定义 mapping
可以解决这个问题。
【讨论】:
以上是关于枚举映射附加约束的条件类型的主要内容,如果未能解决你的问题,请参考以下文章