【问题标题】:How to transform union type to tuple type如何将联合类型转换为元组类型
【发布时间】:2022-05-15 19:03:44
【问题描述】:

例如,我有一个类型:

type abc = 'a' | 'b' | 'c';

如何在编译时创建一个包含并集所有元素的元组类型?

type t = ['a','b', 'c'];

【问题讨论】:

  • 联合只能包含一个其中一个类型列表的值。因此,根据定义,您不能拥有包含 all 值的联合。你想达到什么目的?
  • "keyof {a: number, b: number, c: number}" 返回 'a'|'b'|'c',但我需要元组来做一些元编程的东西
  • 您的实际用例是什么?你能举一个小例子来说明你打算如何在代码中使用元组吗?
  • 我想将 poco 对象转换为类型安全的数组。

标签: typescript tuples union-types


【解决方案1】:

免责声明:不要这样做!如果有人告诉您使用他们在此答案中找到的代码来做任何事情,除了证明为什么这是一个坏主意,快跑!


从元组类型转换为联合类型很容易;例如,请参阅this question。但相反,从联合转换为元组是您不应该尝试做的那些真正糟糕的想法之一。 (讨论和规范答案见microsoft/TypeScript#13298)让我们先做,以后再骂自己:

// oh boy don't do this
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> =
  UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never

// TS4.0+
type Push<T extends any[], V> = [...T, V];

// TS4.1+
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
  true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>

type abc = 'a' | 'b' | 'c';
type t = TuplifyUnion<abc>; // ["a", "b", "c"] 

Playground link

这种工作,但我真的真的非常建议不要将它用于任何官方目的或任何生产代码。原因如下:

  • 您不能依赖联合类型的顺序。这是编译器的实现细节;由于X | Y 等价于Y | X,编译器可以随意更改其中一个。有时确实如此:

      type TypeTrue1A = TuplifyUnion<true | 1 | "a">; // [true, 1, "a"] ?
      type Type1ATrue = TuplifyUnion<1 | "a" | true>; // [true, 1, "a"]!! ?
    

    所以真的没有办法保持顺序。请不要假设输出至少总是上面的[true, 1, "a"];不能保证。这是一个实现细节,因此特定的输出可以从 TypeScript 的一个版本更改为下一个版本,或者从您的代码的一个编译版本更改为下一个版本。这在某些情况下确实会发生:例如,编译器 caches 联合;看似不相关的代码可能会影响将哪个联合排序放入缓存,从而影响出哪个排序。订单不仅仅是不可靠。

  • 您可能对编译器认为联合以及它何时折叠或展开不满意。 "a" | string 将被折叠为string,而boolean 实际上扩展为false | true:

      type TypeAString = TuplifyUnion<"a" | string>; // [string]
      type TypeBoolean = TuplifyUnion<boolean>; // [false, true]
    

    因此,如果您打算保留一些现有数量的元素,则应该停止计划。没有一般的方法可以让一个元组去一个联合并返回而不会丢失这些信息。

  • 没有支持迭代通用联合的方法。我使用的所有技巧都是滥用conditional types。首先,我将联合 A | B | C 转换为函数联合,例如 ()=&gt;A | ()=&gt;B | ()=&gt;C,然后使用 intersection inference trick 将该联合函数转换为函数的交集,例如 ()=&gt;A &amp; ()=&gt;B &amp; ()=&gt;C,这被解释为单个 overloaded函数类型,而使用条件类型提取返回值只会抓取 last 重载。所有这些疯狂最终导致A | B | C 只抽出一个成分,可能是C。然后你必须把它推到你正在构建的元组的末尾。

  • 现在 TypeScript 4.1 引入了recursive conditional types 的原因已经不多了,但是递归条件类型对编译器的负担比您想要的要多,并且递归限制较浅。因此,如果您有超过 20 个元素的联合,或者其类型本身以某种方式递归的联合,您可能会看到一些性能或编译问题。

所以你去。你可以这样做,但不要这样做。 (如果你做做它,如果发生爆炸,不要怪我。?)希望这会有所帮助。祝你好运!

【讨论】:

  • 我读过关于交叉路口干扰技巧的文章。编译器用逆变器推断交集类型,所以它应该用协变推断联合,对吧?但是这个并没有从 interssction 类型 IntersectionToUnion = (U extends any ? U[] : never) extends (infer I)[] 进行联合?我从不;为什么?
  • 因为条件类型分布在联合上,而不是交点上。目前该语言中没有任何内容分布在交叉路口。
  • 这可能很糟糕,但有时我们需要将类型转换为元组。例如,第三方库公开了联合类型,我需要一个元组来在我的应用中制作一些东西
  • @JPS 好吧,当疯狂的事情发生时,我希望你不会责怪我!
  • 我会说联合到交集“技巧”并不是真正的技巧,而是人们对健全类型系统的期望(虽然 TS 通常不是健全的)。这是棘手的重载事情!为避免 TuplifyUnion&lt;"a" | string&gt; 和 TuplifyUnion&lt;boolean&gt; 问题,您可以将并集的每个术语包装在一个函数中。
【解决方案2】:

我有时会遇到一种情况,我想从类型 A 派生类型 B,但发现 TS 不支持它,或者进行转换导致代码难以遵循。有时,从 A 导出 B 的选择是任意的,我也可以从另一个方向导出。在这里,如果您可以从您的元组开始,您可以轻松地派生一个类型,该类型涵盖了元组作为元素接受的所有值:

type X = ["a", "b", "c"];

type AnyElementOf<T extends any[]> = T[number];

type AnyElementOfX = AnyElementOf<X>;

如果您检查AnyElementOfX 的扩展,您将得到"a" | "b" | "c"。

【讨论】:

    【解决方案3】:

    这种方法需要额外的工作,因为它从实际元组派生所需的元组类型。

    “嗯,”你在想,“如果我必须提供我需要的确切类型,那有什么用呢? >?”

    我同意。但是,它对于相关用例仍然有用:确保元组包含联合类型的所有元素(例如,用于单元测试)。在这种情况下,您需要声明数组无论如何,因为 Typescript 无法从类型生成运行时值。因此,“额外”的工作变得毫无意义。

    注意:作为先决条件,我依赖于 TypesEqual 运算符 as discussed here。为了完整起见,这是我正在使用的版本,但还有其他选项:

    type FunctionComparisonEqualsWrapped<T> =
      T extends (T extends {} ? infer R & {} : infer R)
      ? { [P in keyof R]: R[P] }
      : never;
    
    type FunctionComparisonEquals<A, B> =
      (<T>() => T extends FunctionComparisonEqualsWrapped<A> ? 1 : 2) extends
       <T>() => T extends FunctionComparisonEqualsWrapped<B> ? 1 : 2
      ? true
      : false;
    
    type IsAny<T> = FunctionComparisonEquals<T, any>;
    
    type InvariantComparisonEqualsWrapped<T> =
      { value: T; setValue: (value: T) => never };
    
    type InvariantComparisonEquals<Expected, Actual> =
      InvariantComparisonEqualsWrapped<Expected> extends
      InvariantComparisonEqualsWrapped<Actual>
      ? IsAny<Expected | Actual> extends true
        ? IsAny<Expected> | IsAny<Actual> extends true
              ? true
              : false
          : true
      : false;
    
    export type TypesEqual<Expected, Actual> =
      InvariantComparisonEquals<Expected, Actual> extends true
      ? FunctionComparisonEquals<Expected, Actual>
      : false;
    
    export type TypesNotEqual<Expected, Actual> =
      TypesEqual<Expected, Actual> extends true ? false : true;
    

    下面是实际的解决方案和示例用法:

    export function rangeOf<U>() {
      return function <T extends U[]>(...values: T) {
        type Result = true extends TypesEqual<U, typeof values[number]> ? T : never;
        return values as Result;
      };
    }
    
    type Letters = 'a' | 'b' | 'c';
    const letters = rangeOf<Letters>()(['a', 'b', 'c']);
    type LettersTuple = typeof letters;
    

    一些注意事项:rangeOf 不关心元组的顺序,它允许重复条目(例如,['b', 'a', 'c', 'a'] 将满足 rangeOf&lt;Letters&gt;())。对于单元测试,这些问题可能不值得关心。但是,如果您确实关心,您可以在返回之前对 values 进行排序和重复数据删除。这会在初始化期间以少量运行时性能换取“更干净”的表示。

    【讨论】:

      【解决方案4】:

      只是一种解决方案。这很简单,但需要手动编写样板代码。

      LazyStyleLoader 类型的任何对象必须包含ThemeName union 的所有成员作为属性。

      至少,您可以确保您的对象始终对联合体的每个成员都有一个属性。

      export type ThemeName = "default_bootstrap" | "cerulean" | "cosmo" | "cyborg"
      
      type LazyStyleLoader = {
          [key in ThemeName]: () => Promise<typeof import("*?raw")>
      }
      
      export const LazyThemeLoader: LazyStyleLoader = {
          default_bootstrap: () => import("bootstrap/dist/css/bootstrap.min.css?raw"),
          cerulean: () => import("bootswatch/dist/cerulean/bootstrap.min.css?raw"),
          cosmo: () => import("bootswatch/dist/cosmo/bootstrap.min.css?raw"),
          cyborg: () => import("bootswatch/dist/cyborg/bootstrap.min.css?raw"),
      };
      
      

      然后你可以使用Object.keys()得到一个联合成员的元组:

      const tuple = Object.keys(LazyThemeLoader);
      // ["default_bootstrap", "cerulean", "cosmo", "cyborg"]
      

      【讨论】:

      • @OlegValteriswithUkraine,啊,该操作询问是否将联合转换为 type 上下文中的元组。我的解决方案是针对 expression 上下文。无论如何,我将它留在这里,也许它对未来的读者有用。
      • 没关系,我已经看到您编辑了答案以包括从中获取元组的想法(因此删除了注释和投票)。 typeof 类型的查询在这里是可能的。
      猜你喜欢
      • 2020-04-02
      • 1970-01-01
      • 2018-10-26
      • 2020-06-04
      • 1970-01-01
      • 2021-09-03
      • 2021-10-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多