typescript 如何将联合类型转换为元组类型

ecfsfe2w  于 2022-12-27  发布在  TypeScript
关注(0)|答案(5)|浏览(371)

例如,我有一个类型:

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

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

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

ppcbkaq51#

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

从元组类型转换为联合类型很容易;但是相反的,从一个联合体转换成一个元组是那些真正糟糕的想法之一,你不应该尝试去做。(参见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链接
这类的工作,但我真的真的真的不建议使用它为任何官方目的或在任何生产代码。

  • 你不能依赖联合类型的排序,它是编译器的实现细节;因为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到下一个版本,或者从一个代码编译到下一个版本。在某些情况下确实会发生这种情况:例如,编译器 * 缓存 * 联合;看起来不相关的代码可以影响并集的哪种排序被放入缓存,从而影响哪种排序出来。2排序不仅仅是不可靠。

  • 您可能对编译器认为的并集以及它何时折叠或展开不满意。"a" | string将折叠为string,而boolean实际上 * 展开 * 为false | true
type TypeAString = TuplifyUnion<"a" | string>; // [string]
  type TypeBoolean = TuplifyUnion<boolean>; // [false, true]

因此,如果你打算保留一些现有的元素数量,你应该停止这样的计划。没有一种通用的方法可以让一个元组往返于一个联合体而不丢失这些信息。

  • 没有支持的方法来迭代一般的联合体。我使用的技巧是滥用条件类型。首先我将一个联合体A | B | C转换为一个函数的联合体,如()=>A | ()=>B | ()=>C,然后使用交集推理技巧将该函数的联合体转换为一个函数的交集,如()=>A & ()=>B & ()=>C,它被解释为一个单一的重载函数类型。而使用条件类型来提取返回值只会占用 * last * 重载,所有这些疯狂的操作最后都会占用A | B | C并只提取一个组成部分,可能是C,然后你必须将其推到正在构建的元组的末尾。
  • TypeScript 4.1引入了递归条件类型,但递归条件类型给编译器带来的负担比您想要的更大,并且递归限制也较浅。因此,如果您的联合包含超过20个元素,或者联合的类型本身在某种程度上是递归的,则可能会遇到一些性能或编译问题。

So there you go. You can kind of do it, but don't do it. (And if you do do it, don't blame me if something explodes. 💣) Hope that helps. Good luck!

oyt4ldly

oyt4ldly2#

我有时候会遇到这样的情况:我想从类型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"

hrysbysz

hrysbysz3#

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

  • "嗯,"* 你在想,"如果我必须提供需要的确切类型,那有什么用?"

我同意。但是,它对相关用例仍然有用:* * 确保元组包含联合类型的所有元素**(例如,用于单元测试)。在这种情况下,您需要声明数组 * 无论如何 *,因为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<Letters>())。对于单元测试,这些问题可能不值得关心。但是,如果你关心,你可以在返回values之前对其进行排序和删除重复。这在初始化期间牺牲了少量的运行时性能,以获得"更干净"的表示。

xdyibdwo

xdyibdwo4#

再进一步看jcalzanswer(同样,不要使用他的答案,他所有的警告都适用!),您实际上可以创建一些安全和可预测的东西。

// Most of this is jcalz answer, up until the magic.
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;

type Push<T extends any[], V> = [...T, V];

type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
    true extends N
        ? []
        : Push<TuplifyUnion<Exclude<T, L>>, L>;

// The magic happens here!
export type Tuple<T, A extends T[] = []> =
    TuplifyUnion<T>['length'] extends A['length']
        ? [...A]
        : Tuple<T, [T, ...A]>;

这样做的原因是因为TypeScript中的tuple只是另一个Array,我们可以比较数组的长度。
现在,我们可以创建一个长度为联合成员数的元组,其中每个位置都是泛型类型T,而不是编译器行为导致的TuplifyUnion的不可预测排序。

const numbers: Tuple<2 | 4 | 6> = [4, 6, 2];
// Resolved type: [2 | 4 | 6, 2 | 4 | 6, 2 | 4 | 6]

不可否认,这仍然是不完美的,因为我们不能保证每个成员都是联合体的唯一成员,但是我们现在保证了联合体的所有成员的计数都将被结果元组覆盖,这是可预测的,也是安全的。

roqulrg3

roqulrg35#

只是一种解决方案。这很简单,但是需要手工编写样板代码。
类型为LazyStyleLoader的任何对象必须包含ThemeName联合的所有**成员作为属性。
至少,您可以确定您的对象对于联合的每个成员总是具有一个属性。

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"]

相关问题