【问题标题】:Conditional type for additional constraint on enum mapping枚举映射附加约束的条件类型
【发布时间】:2021-09-23 07:36:50
【问题描述】:

在我的项目中,我有两个枚举 SourceEnumTargetEnum。对于这两个枚举,都存在一个函数,该函数使用一些取决于枚举值的参数调用。预期参数的确切类型由SourceParamsTargetParams 两种类型映射定义。

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

标签: typescript types typescript-types


【解决方案1】:

因此,您在这里期望 Typescript 看到 &lt;T extends TargetEnum&gt;(): 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;

我们无法限制ST,因此我们必须在模板中使用更多条件。 (是的,这也很奇怪;改变这样的行为是相当违反直觉的。)我们也不能在这里简单地硬编码整个TargetEnum,即使这最终是我们想要的——分布必须是跨越一个不受约束的参数到类型别名。

当我们完成那个后,我们就可以在ConstrainedMapping中使用它了:

type ConstrainedMapping = {
  [S in SourceEnum]: () => TargetWithMatchingParams<S, TargetEnum>;
};

请注意,该函数不再是通用的(这是您的问题的一部分),我们通过将 TargetEnum 传递给 TargetWithMatchingParams 来实现您正在寻找的分布。

顺便说一句,如果您的真实案例像您的示例一样是静态的,则从 ConstrainedMapping 的定义中删除 () =&gt; 并使用 mapping[x] 而不是 mapping[x]() “调用”mapping 会稍微提高性能,并且可以说更容易阅读。

最后,这里有几个限制需要注意。

  1. 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 不够聪明,无法真正理解SourceParamsTargetParams 之间的关系,并且无法识别任何有效的SourceParams 值将始终匹配对应的TargetParams

  2. 在我们接受源联合和源参数联合的情况下,基本上是上述的非泛型版本,Typescript 允许不安全的值。考虑:

    function baz(src: SourceEnum, params: SourceParams[SourceEnum]) {
      targetFn(mapping(src), params);
    }
    baz(SourceEnum.SOURCE_A, { paramB: 42 });
    

    这不会导致任何错误,即使我们有 SOURCE_AparamB 并且这是一个无效的组合。不过,归根结底,这只是对 Typescript 联合工作方式的限制——问题在于 baz 的定义或 SourceParams/TargetParams 的定义,如果 baz 调用 @ 则完全相同987654358@ 根本没有mapping 进入。

这里的太长,未阅读版本只是确保您使用特定枚举类型而不是整个枚举调用 sourceFntargetFn。 Typescript 不会跟踪单独变量之间的关系,因此它不会检查未知的 SourceEnum 和未知的 SourceParams[SourceEnum] 是否真的在一起,并且没有定义 mapping 可以解决这个问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多