枚举映射附加约束的条件类型
Conditional type for additional constraint on enum mapping
在我的项目中,我有两个枚举 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 中的解决方案,但未来版本中的解决方案对我来说也很好。
所以你在这里期望的是让 Typescript 看到 <T extends TargetEnum>(): SourceParams[K] extends TargetParams[T] ? T : never;
,然后将 T
的所有可能值分配到这个条件中,并创建一个为真值的并集。
问题是,Typescript 没有这样做。它将 T
视为未知,并且除了将 { paramA: string; }
替换为 SourceParams[K]
之外,不再对该表达式求值。您要查找的分布仅出现在 Distributive Conditional Types 中。所以我们必须重写你的ConstrainedMapping
来使用一个。
分布式条件类型是一种条件类型别名,其中 none 个参数受到约束。所以首先,我们需要一个实际的 type
声明这个 return 值,我们不能把它放在更大的 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
而不是 mapping[x]()
会稍微提高性能,并且可以说更容易阅读。
最后,这里有一些限制需要注意。
在 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
调用了 sourceFn
而 mapping
根本没有进入它。
这里太长,没有阅读的版本只是确保你用特定的枚举类型调用 sourceFn
和 targetFn
,而不是整个枚举。 Typescript 不会跟踪单独变量之间的关系,因此它不会检查您的未知 SourceEnum
和未知 SourceParams[SourceEnum]
是否真的在一起,并且 mapping
的定义不会解决那个问题。
在我的项目中,我有两个枚举 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 中的解决方案,但未来版本中的解决方案对我来说也很好。
所以你在这里期望的是让 Typescript 看到 <T extends TargetEnum>(): SourceParams[K] extends TargetParams[T] ? T : never;
,然后将 T
的所有可能值分配到这个条件中,并创建一个为真值的并集。
问题是,Typescript 没有这样做。它将 T
视为未知,并且除了将 { paramA: string; }
替换为 SourceParams[K]
之外,不再对该表达式求值。您要查找的分布仅出现在 Distributive Conditional Types 中。所以我们必须重写你的ConstrainedMapping
来使用一个。
分布式条件类型是一种条件类型别名,其中 none 个参数受到约束。所以首先,我们需要一个实际的 type
声明这个 return 值,我们不能把它放在更大的 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
而不是 mapping[x]()
会稍微提高性能,并且可以说更容易阅读。
最后,这里有一些限制需要注意。
在
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
调用了sourceFn
而mapping
根本没有进入它。
这里太长,没有阅读的版本只是确保你用特定的枚举类型调用 sourceFn
和 targetFn
,而不是整个枚举。 Typescript 不会跟踪单独变量之间的关系,因此它不会检查您的未知 SourceEnum
和未知 SourceParams[SourceEnum]
是否真的在一起,并且 mapping
的定义不会解决那个问题。