microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.41k stars 12.41k forks source link

Type manipulations: union to tuple #13298

Closed krryan closed 5 years ago

krryan commented 7 years ago

A suggestion to create a runtime array of union members was deemed out of scope because it would not leave the type system fully erasable (and because it wouldn't be runtime complete, though that wasn't desired). This suggestion is basically a variant of that one that stays entirely within the type domain, and thus stays erasable.

The suggestion is for a keyword similar to keyof that, when given a union type, would result in a tuple type that includes each possibility in the union.

Combined with the suggestion in this comment to instead implement a codefix to create the array literal, this could be used to ensure that 1. the array was created correctly to begin with, and 2. that any changes to the union cause an error requiring the literal array to be updated. This allows creating test cases that cover every possibility for a union.

Syntax might be like this:

type SomeUnion = Foo | Bar;

type TupleOfSomeUnion = tupleof SomeUnion; // has type [Foo, Bar]

type NestedUnion = SomeUnion | string;

type TupleOfNestedUnion = tupleof NestedUnion; // has type [Foo, Bar, string]

Some issues I foresee:

  1. I don't know what ordering is best (or even feasible), but it would have to be nailed down in some predictable form.

  2. Nesting is complicated.

  3. I expect generics would be difficult to support?

  4. Inner unions would have to be left alone, which is somewhat awkward. That is, it would not be reasonable to turn Wrapper<Foo|Bar> into [Wrapper<Foo>, Wrapper<Bar>] even though that might (sometimes?) be desirable. In some cases, it’s possible to use conditional types to produce that distribution, though it has to be tailored to the particular Wrapper. Some way of converting back and forth between Wrapper<Foo|Bar> and Wrapper<Foo>|Wrapper<Bar> would be nice but beyond the scope of this suggestion (and would probably require higher-order types to be a thing).

  5. My naming suggestions are weak, particularly tupleof.

NOTE: This suggestion originally also included having a way of converting a tuple to a union. That suggestion has been removed since there are now ample ways to accomplish that. My preference is with conditional types and infer, e.g. ElementOf<A extends unknown[]> = A extends (infer T)[] ? T : never;.

fightingcat commented 5 years ago

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

@jituanlin This is called distributive condition types, has been explained here.

hediet commented 5 years ago
type Overwrite<T, S extends any> = { [P in keyof T]: S[P] };
type TupleUnshift<T extends any[], X> = T extends any ? ((x: X, ...t: T) => void) extends (...t: infer R) => void ? R : never : never;
type TuplePush<T extends any[], X> = T extends any ? Overwrite<TupleUnshift<T, any>, T & { [x: string]: X }> : never;

type UnionToTuple<U> = UnionToTupleRecursively<[], U>;

type UnionToTupleRecursively<T extends any[], U> = {
    1: T;
    0: UnionToTupleRecursively_<T, U, U>;
}[[U] extends [never] ? 1 : 0]

type UnionToTupleRecursively_<T extends any[], U, S> =
    S extends any ? UnionToTupleRecursively<TupleUnshift<T, S> | TuplePush<T, S>, Exclude<U, S>> : never;

let x: UnionToTuple<1 | 2 | 3 | 4> = [1, 2, 3, 4];
let y: UnionToTuple<1 | 2 | 3 | 4> = [4, 3, 2, 1];

Union to all the permutation of tuples, by generating n! unions.....%20extends%20(...t%3A%20infer%20R)%20%3D%3E%20void%20%3F%20R%20%3A%20never%20%3A%20never%3B%0D%0Atype%20TuplePush%3CT%20extends%20any%5B%5D%2C%20X%3E%20%3D%20T%20extends%20any%20%3F%20Overwrite%3CTupleUnshift%3CT%2C%20any%3E%2C%20T%20%26%20%7B%20%5Bx%3A%20string%5D%3A%20X%20%7D%3E%20%3A%20never%3B%0D%0A%0D%0Atype%20UnionToTuple%3CU%3E%20%3D%20UnionToTupleRecursively%3C%5B%5D%2C%20U%3E%3B%0D%0A%0D%0Atype%20UnionToTupleRecursively%3CT%20extends%20any%5B%5D%2C%20U%3E%20%3D%20%7B%0D%0A%20%20%20%201%3A%20T%3B%0D%0A%20%20%20%200%3A%20UnionToTupleRecursively%3CT%2C%20U%2C%20U%3E%3B%0D%0A%7D%5B%5BU%5D%20extends%20%5Bnever%5D%20%3F%201%20%3A%200%5D%0D%0A%0D%0Atype%20UnionToTupleRecursively%3CT%20extends%20any%5B%5D%2C%20U%2C%20S%3E%20%3D%0D%0A%20%20%20%20S%20extends%20any%20%3F%20UnionToTupleRecursively%3CTupleUnshift%3CT%2C%20S%3E%20%7C%20TuplePush%3CT%2C%20S%3E%2C%20Exclude%3CU%2C%20S%3E%3E%20%3A%20never%3B%0D%0A%0D%0Alet%20x%3A%20UnionToTuple%3C1%20%7C%202%20%7C%203%20%7C%204%3E%20%3D%20%5B1%2C%202%2C%203%2C%204%5D%3B%0D%0Alet%20y%3A%20UnionToTuple%3C1%20%7C%202%20%7C%203%20%7C%204%3E%20%3D%20%5B4%2C%203%2C%202%2C%201%5D%3B%0D%0A)

@fightingcat Just for reference, there is a cleaner way to destructure a tuple:

export type DestructureTuple<T extends any[]> = T extends []
    ? false
    : ((...tuple: T) => void) extends ((
            first: infer TFirst,
            ...rest: infer TRest
      ) => void)
    ? { first: TFirst; rest: TRest }
    : false;
fightingcat commented 5 years ago

@hediet But there is no use of destructuring in this demonstration...

Griffork commented 5 years ago

If this is never going to be made into a keyword, can someone make and maintain a library for it? I'm loathe to include typing code that I don't understand into my project in case it breaks in an update (I use all the strict flags) and as fluent in typescript's types as I claim to be, I can't get my head around that mess of typings even after spending hours picking it apart.

I'm truly amazed that this isn't a candidate for inclusion given how often I've needed it, whereas things like Partial are.

dragomirtitian commented 5 years ago

@Griffork The problem here is multi-fold and has been explained in this thread, some highlights:

  1. Conceptually unions are un-ordered, tuples are ordered
  2. The current implementation does not maintain a consistent union ordering and changing this would be difficult

Partial is a simple application of mapped types, it's easy to include that in the default library.

All variations of tuple to union suffer from issues. They all use unsupported recursive types aliases which is already a red flag.

The version that generates a single tuple IS NOT STABLE BETWEEN BUILDS. Small code changes in completely unrelated code can easily make the compiler create the union in a different order and thus the tuple will be in a different order and this will break your code seemingly at random.

The version that generates all possible permutations is a performance trap. For a small number of union constituents it will probably work decently (willing to be wrong here) but consider what generating all permutations means for a union with 10 constituents. The number of all permutations is 10! (3628800). That is a huge number of types from a relatively small union. Adding such a type would need to come with the warning use for up to a maximum of 8 which would just be bad.

Given all these issues I would run as fast as I can from any library delivering a type called UnionToTuple. Any such library would be hiding major problems from its consumers and would be behaving irresponsibly IMO.

RyanCavanaugh commented 5 years ago

I wish I could 👍 @dragomirtitian's comment more than once

AnyhowStep commented 5 years ago

I would like to take this one step further and ask for an RNG type (because lulz). The RNG<U> type should take a union of types and return a random one.

//Mouseover this
type a = RNG<1|2|3|4>;
//Mouseover this
type b = RNG<1|2|3|4>;

Every time you mouse over it, it should give you a different result. Every time you build, it gives you a different result. Every time it resolves, it gives you a different result.

I wouldn't be surprised if a and b have different types most of the time.

Please implement this /s

AnyhowStep commented 5 years ago

Simpler version of the permutations approach,

type PushFront<TailT extends any[], HeadT> = (
  ((head : HeadT, ...tail : TailT) => void) extends ((...arr : infer ArrT) => void) ?
  ArrT :
  never
);
type CalculatePermutations<U extends string, ResultT extends any[]=[]> = (
    {
        [k in U] : (
            Exclude<U, k> extends never ?
            PushFront<ResultT, k> :
            CalculatePermutations<Exclude<U, k>, PushFront<ResultT, k>>
        )
    }[U]
);

type elements =|"z"|"y"|"x"|"a"
//type p = ["a", "x", "y", "z"] | ["x", "a", "y", "z"] | ["a", "y", "x", "z"] | ["y", "a", "x", "z"] | ["x", "y", "a", "z"] | ["y", "x", "a", "z"] | ["a", "x", "z", "y"] | ["x", "a", "z", "y"] | ... 15 more ... | [...]
type p = CalculatePermutations<elements>

Playground

Don't try it with 10 elements.

danieldietrich commented 5 years ago

Hi @RyanCavanaugh, I could really use your help with this one.

My use case is recursively merging objects. A real world use-case would be to merge OpenAPI specifications (denoted in JSON) of multiple Microservices in order to produce a consolidated API specification for an API Gateway (on top of the Microservices).

I've create such a merge function, published here.

The merge algorithm is roughly as follows. Given non-null and non-undefined input objects,

Example:

// = {a: "2", b: [1, "2"]}
const o = mergeObjects(
    {a: 1, b: [1]},
    {a: "2", b: ["2"]},
    null,
    undefined
);

I managed to infer the result type by

The signature looks like this:

function mergeObjects<T extends Obj>(...objects: T[]): Merged<T>

Currently, the inferred type Merged of the the example above is:

const o: {
    a: number;
    b: number[];
} | {
    a: string;
    b: string[];
}

Problem:

// infers to number | string
const x = o.a

// infers to number[] | string[]
const x = o.b

Expected result:

// infers to string (<--- last element of the union number | string)
const x = o.a

// infers to Array<number | string> resp. (number | string)[]
const x = o.b

Missing parts:

// (T extends Array<infer U> | Array<infer V> | ...)
//   ? Array<U | V | ...>
//   : ...
type ZipArrayUnion<T> = T; // TODO: ???

// (T extends U | V) ? V : ...
type LastUnionElement<T> =
    // TODO: ??? add case to select last union element
    T extends JSONObject ? { [K in keyof T]: LastUnionElement<T[K]> } :
    T;

I came here because I thought the last element of a union could be statically calculated using tuples.

Complete types:

// Type of input objects
type Obj = JSONObject | null | undefined;

// Output type = merged inputs
type Merged<T extends Obj> = LastUnionElement<ZipArrayUnion<NoUndefinedField<NonNullable<T>>>>;

// Removes fields with undefined values
type NoUndefinedField<T> =
    T extends JSONArray ? T :
    T extends JSONObject ? { [P in keyof T]-?: NoUndefinedField<NotUndefined<T[P]>> } :
    T;

// Does not permit undefined
type NotUndefined<T> = T extends undefined ? never : T;

// (T extends Array<infer U> | Array<infer V> | ...)
//   ? Array<U | V | ...>
//   : ...
type ZipArrayUnion<T> = T; // TODO: ???

// (T extends U | V) ? V : ...
type LastUnionElement<T> =
    // TODO: ??? add case to select last union element
    T extends JSONObject ? { [K in keyof T]: LastUnionElement<T[K]> } :
    T;

// A recursive JSON definition, permitsundefined
type JSONValue =
    | string
    | number
    | boolean
    | null
    | undefined
    | JSONObject
    | JSONArray;

interface JSONObject {
    [x: string]: JSONValue;
}

interface JSONArray extends Array<JSONValue> { }

TypeScript's type system is said to be Turing complete, so theoretically it must be possible.

I would be glad to hear some feedback or comments!

Thanks,

Daniel


Update

Looking at lib.es2015.core.d.ts shows, that Object.assign does an intersection & instead of a union |. Projected to my case, mergeObjects currently infers a union T of input objects. I just need to turn that into an intersection when calculating the result type:

type Obj = Record<string, unknown>;

type Merged<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

function mergeObjects<T extends Obj | null | undefined>(...objects: T[]): Merged<NonNullable<T>>

That way, merging {a: number} and {a: string} will lead to {a: number & string}, which is effectively {a: never}. On the type level, that makes sense to me.

Note: lib.es2015.core.d.ts currently does not seem to be that accurate for > 4 input parameters.

AnyhowStep commented 5 years ago

You don't need union to tuple for this.

Also, ask on stack overflow or gitter


Also, you want mergeObjects to infer a tuple type for its input. And you'll want a recursive type alias to calculate the output.

treybrisbane commented 4 years ago

Just for fun, I decided to see how much simpler the union -> tuple implementation would be with the recent improvements around recursive type aliases. Turns out, noticeably! 😄

type TupleHead<Tuple extends any[]> =
    Tuple extends [infer HeadElement, ...unknown[]] ? HeadElement : never;

type TupleTail<Tuple extends any[]> =
    ((...args: Tuple) => never) extends ((a: any, ...args: infer TailElements) => never)
    ? TailElements
    : never;

type TuplePrepend<Tuple extends any[], NewElement> =
    ((h: NewElement, ...t: Tuple) => any) extends ((...r: infer ResultTuple) => any) ? ResultTuple : never;

type Consumer<Value> = (value: Value) => void;

type IntersectionFromUnion<Union> =
    (Union extends any ? Consumer<Union> : never) extends (Consumer<infer ResultIntersection>)
    ? ResultIntersection
    : never;

type OverloadedConsumerFromUnion<Union> = IntersectionFromUnion<Union extends any ? Consumer<Union> : never>;

type UnionLast<Union> = OverloadedConsumerFromUnion<Union> extends ((a: infer A) => void) ? A : never;

type UnionExcludingLast<Union> = Exclude<Union, UnionLast<Union>>;

type TupleFromUnionRec<RemainingUnion, CurrentTuple extends any[]> =
    [RemainingUnion] extends [never]
    ? { result: CurrentTuple }
    : { result: TupleFromUnionRec<UnionExcludingLast<RemainingUnion>, TuplePrepend<CurrentTuple, UnionLast<RemainingUnion>>>['result'] };

export type TupleFromUnion<Union> = TupleFromUnionRec<Union, []>['result'];

// ------------------------------------------------------------------------------------------------

interface Person {
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;
}

const keysOfPerson: TupleFromUnion<keyof Person> = ["firstName", "lastName", "dob", "hasCats"];

Playground link

(Something, something, probably don't use this in your projects, something)

Harpush commented 4 years ago

I ended up here looking for union to tuple but more like the first comments. I don't want a real union to tuple as i understand and agree that there is no way to know the order especially if the union came from keyof for example. But what i do want is a way to say this tuple needs to have exactly all the union members - which means no additional items not in the union and no less than the union items and no duplicated while the order doesn't matter. The use case is when i declare an union and want to create an array with all union items - as of now if i added a new union member the compiler wont complain i forgot to add it to the array (the same power we have with records based on union key). I did some tests myself and succeeded in creating such type but it requires a phantom function - i do hope an easier alternative will be implemented. Posting the code if everyone wishes to use it - and credit to both examples given here and this stackoverflow question:

type TupleToUnionWithoutDuplicated<A extends ReadonlyArray<any>> = {
  [I in keyof A]: unknown extends {
    [J in keyof A]: J extends I ? never : A[J] extends A[I] ? unknown : never;
  }[number]
    ? never
    : A[I];
}[number];
type TupleToUnionOnlyDuplicated<A extends ReadonlyArray<any>> = Exclude<
  A[number],
  TupleToUnionWithoutDuplicated<A>
>;
}[number];
type HasUnionMissing<Desired, Actual> = Exclude<Desired, Actual> extends never
  ? false
  : true;
type Missing<Desired, Actual> = Exclude<Desired, Actual> extends never
  ? never
  : Exclude<Desired, Actual>;
type HasUnionExtra<Desired, Actual> = Exclude<Actual, Desired> extends never
  ? false
  : true;
type Extra<Desired, Actual> = Exclude<Actual, Desired> extends never
  ? never
  : Exclude<Actual, Desired>;
type Error<Union, Msg> = [Union, 'is/are', Msg];
type AllUnionTuple<K, T extends ReadonlyArray<any>> = HasUnionExtra<
  K,
  T[number]
> extends true
  ? Error<Extra<K, T[number]>, 'extra'>
  : HasUnionMissing<K, T[number]> extends true
  ? Error<Missing<K, T[number]>, 'missing'>
  : T[number] extends TupleToUnionWithoutDuplicated<T>
  ? T
  : Error<TupleToUnionOnlyDuplicated<T>, 'duplicated'>;

const asAllUnionTuple = <T>() => <U extends ReadonlyArray<any>>(
  cc: AllUnionTuple<T, U>
) => cc;
type keys = 'one' | 'two' | 'three';
// Hovering ee will show the same const type given 
// and the function invocation will error if anything is not correct.
const ee = asAllUnionTuple<keys>()(['one', 'two', 'three'] as const);
karol-majewski commented 4 years ago

That's how I create exhaustive tuples from unions:

type ValueOf<T> = T[keyof T];

type NonEmptyArray<T> = [T, ...T[]]

type MustInclude<T, U extends T[]> =
  [T] extends [ValueOf<U>]
    ? U
    : never;

const enumerate = <T>() =>
  <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) =>
    elements;

Playground link

Usage

type Color = 'red' | 'blue';

enumerate<Color>()();               // ⛔️ Empty lists are not allowed!
enumerate<Color>()('red');          // ⛔️ Incomplete
enumerate<Color>()('red', 'red');   // ⛔️ Duplicates are not allowed
enumerate<Color>()('red', 'green'); // ⛔️ Intruder! 'green' is not a valid Color

enumerate<Color>()('red', 'blue');  // ✅ Good
enumerate<Color>()('blue', 'red');  // ✅ Good

Use case

Type guards. Using enumerate keeps them simple and always up-to-date. If your union is made of primitive types, you can simply use Array#includes.

const colors = enumerate<Color>()('blue', 'red');

const isColor = (candidate: any): candidate is Color =>
  colors.includes(candidate);

When you add another member to Color, the existing implementation will error, urging you to register the new value.

treybrisbane commented 4 years ago

Annnnnnd a new version based on the latest variadic tuple and recursive conditional type features! 😛

type TupleHead<Tuple extends readonly unknown[]> =
    Tuple extends [infer HeadElement, ...readonly unknown[]] ? HeadElement : never;

type TupleTail<Tuple extends readonly unknown[]> =
    Tuple extends [unknown, ...infer TailElements] ? TailElements : never;

type TuplePrepend<Tuple extends readonly unknown[], NewElement> =
    [NewElement, ...Tuple]

type Consumer<Value> = (value: Value) => void;

type IntersectionFromUnion<Union> =
    (Union extends unknown ? Consumer<Union> : never) extends (Consumer<infer ResultIntersection>)
    ? ResultIntersection
    : never;

type OverloadedConsumerFromUnion<Union> = IntersectionFromUnion<Union extends unknown ? Consumer<Union> : never>;

type UnionLast<Union> = OverloadedConsumerFromUnion<Union> extends ((a: infer A) => void) ? A : never;

type UnionExcludingLast<Union> = Exclude<Union, UnionLast<Union>>;

type TupleFromUnionRec<RemainingUnion, CurrentTuple extends readonly unknown[]> =
    [RemainingUnion] extends [never]
    ? CurrentTuple
    : TupleFromUnionRec<UnionExcludingLast<RemainingUnion>, TuplePrepend<CurrentTuple, UnionLast<RemainingUnion>>>;

export type TupleFromUnion<Union> = TupleFromUnionRec<Union, []>;

// ------------------------------------------------------------------------------------------------

interface Person {
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;
}

const keysOfPerson: TupleFromUnion<keyof Person> = ["firstName", "lastName", "dob", "hasCats"];

Playground link

pushkine commented 4 years ago

One-liner from @AnyhowStep's solution

/**
 * Returns tuple types that include every string in union
 * TupleUnion<keyof { bar: string; leet: number }>; 
 * ["bar", "leet"] | ["leet", "bar"];
 */
type TupleUnion<U extends string, R extends string[] = []> = {
    [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U] & string[];
interface Person {
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;
}
type keys = TupleUnion<keyof Person>; //  ["firstName", "lastName", "dob", "hasCats"] | ... 22 more ... | [...]

link

robarchibald commented 3 years ago

Wow! Amazing thread! I got here looking for a solution to what I thought was a very simple problem. Thanks to the discussion I see this is much more complicated than I thought. Anyway, here's the problem I'm trying to solve. I've got a type that represents the arguments to a function I'm calling:

export type MyArgs = {
  arg1: string;
  arg2: number;
  arg3: string;
};

I can do this for my function type easily enough:

type myFunc = (args: MyArgs) => void;

But I want to be able to do this:

type myFunc = (...args: TupleOf<MyArgs>) => void;

Typescript didn't like the TupleUnion when I tried it.

tjjfvi commented 3 years ago

One-liner, supporting non-(keyof any) types, (ab)using typescript's internal union order:

class BHAAL { private isBhaal = true; }

type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

type Tuple = UnionToTuple<2 | 1 | 3 | 5 | 10 | -9 | 100 | 1001 | 102 | 123456 | 100000000 | "alice" | [[[BHAAL]]] | "charlie">;
//     ^? = [2, 1, 3, 5, 10, -9, 100, 1001, 102, 123456, 100000000, "alice", [[[BHAAL]]], "charlie"]

Playground Link

RyanCavanaugh commented 3 years ago

There's no inherent ordering of properties, so this will break if you look at it funny. Please just don't 😅

tjjfvi commented 3 years ago

@robarchibald

type ValueTuple<O, T extends keyof O = keyof O> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...ValueTuple<O, Exclude<T, W>>, O[Extract<W, keyof O>]]
        : []
);

type MyArgs = {
  arg1: string;
  arg2: number;
  arg3: string;
};

type F = (...args: ValueTuple<MyArgs>) => void;

PlaygroundLink

(Requires nightly until 4.1 lands)

(This is academic; don't ever use this)

jo32 commented 3 years ago

One-liner, supporting non-(keyof any) types, (ab)using typescript's internal union order:

class BHAAL { private isBhaal = true; }

type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

type Tuple = UnionToTuple<2 | 1 | 3 | 5 | 10 | -9 | 100 | 1001 | 102 | 123456 | 100000000 | "alice" | [[[BHAAL]]] | "charlie">;
//     ^? = [2, 1, 3, 5, 10, -9, 100, 1001, 102, 123456, 100000000, "alice", [[[BHAAL]]], "charlie"]

Playground Link

For those who want to understand, I break down the solution a little bit.

type Input = 1 | 2;

type UnionToIntersection<U> = (
  U extends any ? (arg: U) => any : never
) extends (arg: infer I) => void
  ? I
  : never;

type UnionToTuple<T> = UnionToIntersection<(T extends any ? (t: T) => T : never)> extends (_: any) => infer W
  ? [...UnionToTuple<Exclude<T, W>>, W]
  : [];

type Output = UnionToTuple<Input>;

I have a question, when you infer the return of type like ((arg: any) => true) & ((arg: any) => false), the return is false

type C = ((arg: any) => true) & ((arg: any) => false);
type D = C extends (arg: any) => infer R ? R : never; // false;

but logically type like ((arg: any) => true) & ((arg: any) => false) should be never because the return ture and false are mutual exclusive -- you can never find a return is both true and false.

ghost commented 3 years ago

There's also this ugly hack, it doesn't break if you change the order but it only works with string | symbol unions

type Union = 'A' | 'B' | 'C'

type UnionToKeys<U extends string | symbol> = { [K in U]: '' }

const test1: UnionToKeys<Union> = {
    'B': '',
    'C': '',
    'A': '',
}

const tuple = Object.keys(test1)

// ✅ ["B", "C", "A"]
console.log(tuple)

// ⛔️ Property 'B' is missing in type '{ A: ""; C: ""; }' but required in type 'UnionToKeys<Union>'.(2741)
const test2: UnionToKeys<Union> = {
    'A': '',
    'C': '',
}

// ⛔️ Object literal may only specify known properties, and ''D'' does not exist in type 'UnionToKeys<Union>'.(2322)
const test3: UnionToKeys<Union> = {
    'A': '',
    'B': '',
    'C': '',
    'D': '',
}

My use case is that I want to do validation similiar to this

// This type may come from a library that you are using
type ActionsInSomeLibrary = 'create' | 'read' | 'update' | 'delete' | 'aggregate';

type AllowedActions = Exclude<ActionsInSomeLibrary, 'aggregate'>;

const actions: UnionToTuple<AllowedActions> = ['create', 'read', 'update', 'delete'];

actions.includes(requestedAction) // Validate

What benefit does this provide over AllowedActions[]? With just AllowedActions[] you are allowed to do const actions: AllowedActions[] = []; and const actions: AllowedActions[] = ['create', 'create', 'create']; which are not what is actually intended.

ThreePalmTrees commented 3 years ago

People out here pasting an entire application and calling their variables insane names like AxeaPUUX32 :D

Any straight forward method ?

Something as simple as this maybe but the other way around

const allSuits = ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = typeof allSuits[number];  // "hearts" | "diamonds" | "spades" | "clubs"

source

vojtechhabarta commented 3 years ago

We have union of string literal types from existing API (discriminant property) and we need exactly the same set of strings in runtime. I think solution in this case could be to write enum (instead of tuple) and type-check if it is in sync with given union. Here is an example based on @joaopaulobdac solution:

type Union = 'A' | 'B' | 'C'

type UnionToKeys<U extends string> = { [K in U]: number }

enum E {
    A, B, D,
}

// ⛔️  Property 'C' is missing in type 'typeof E' but required in type 'UnionToKeys<Union>'.
const testMissingInE: UnionToKeys<Union> = E;

// ⛔️  Property 'D' is missing in type 'UnionToKeys<Union>' but required in type 'typeof E'.
const testExtraInE: typeof E = testMissingInE;

Could it be possible to get rid of those two constants in runtime and replace theme with some pure compile time checks?

Roaders commented 3 years ago

Sorry this has been stuck in Need Investigation so long!

My primary question is: What would this be useful for? Hearing about use cases is really important; the suggestion as it stands seems like an XY problem situation.

I am late to the party here but this would be useful for me. My use case is that I want to create a Json Schema from an interface at compile time. To do this I will map the interface type to a JSON schema type. TO retain the allowed values of a string union I will need to map the union to a Tuple to add to the json schema.

eugene-kim commented 3 years ago

@RyanCavanaugh my use case:

I'm working with .ts file representing a GraphQL schema that's been generated for me. I need to create objects that match the shape of some types in the schema using the keys of some of the objects in the schema. Given that I only have the types, some kind of mechanism in Typescript to guarantee that the array of keys I'm using only and completely contains elements in the union (which I have via keyof) would be useful.

Order does not matter here

tatemz commented 3 years ago

@jo32's solution worked for me, but I turn off use of any throughout my projects. Here is @jo32's solution with the conditional types swapped after extending never (instead of extends any)

type UnionToIntersection<U> = (
  U extends never ? never : (arg: U) => never
) extends (arg: infer I) => void
  ? I
  : never;

type UnionToTuple<T> = UnionToIntersection<
  T extends never ? never : (t: T) => T
> extends (_: never) => infer W
  ? [...UnionToTuple<Exclude<T, W>>, W]
  : [];
jasonkuhrt commented 3 years ago

@tatemz with that I get this error:

CleanShot 2021-09-18 at 12 16 48@2x

TS 4.4.2

jhunterkohler commented 3 years ago

@jasonkuhrt This is likely a problem inherent in TypeScript due to the size of your union. It has a limit on the depth of recursive calculation. With @tatemz 's solution, 4.5 is allowing me something like 47 long string properties while only ~35 in 4.4. It's odd, but so is Typescript 😆

tatemz commented 3 years ago

Documenting some findings. I originally found this thread because I was interested in taking a Union (e.g. "foo" | "bar") and then map it to some new type. Additionally, I found this thread when I needed to build out n permutations of a union.

That said, it turns out most of my use-cases can be solved by this answer by using a distributive conditional type.

Example (playground link)

type MyLiterals = "foo" | "bar";

type MyLiteralsMappedToObjects<T extends MyLiterals> = T extends never ? never : { value: MyLiteral };
fdcds commented 2 years ago

If you find yourself here wishing you had this operation, PLEASE EXPLAIN WHY WITH EXAMPLES, we will help you do something that actually works instead.

I would like to find a solution for the following:

type MyProps = {
  booleanProp: boolean;
  optionalDateProp?: Date;
};

const myJSONObject = {
  "booleanProp": "false",
  "optionalDateProp": "2021-11-25T12:00:00Z"
};

const coerce = (o: any): MyProps => /* ??? */
coerce(myJSONObject) /* = {
  booleanProp: false,
  optionalDateProp: new Date("2021-11-25T12:00:00Z")
} */

I wish for this operation, so I can implement coerce like this:

// https://stackoverflow.com/a/66144780/11630268
type KeysWithValsOfType<T, V> = keyof {
  [P in keyof T as T[P] extends V ? P : never]: P;
} &
  keyof T;

type MyPropsOfTypeDate = KeysWithValsOfType<MyProps, Date> | KeysWithValsOfType<MyProps, Date | unknown>;
const myPropsOfTypeDate = new Set(/* ??? */);
type MyPropsOfTypeBoolean = KeysWithValsOfType<MyProps, boolean> | KeysWithValsOfType<MyProps, boolean | unknown>;
const myPropsOfTypeBoolean = new Set(/* ??? */);

type MyPropKeys = keyof MyProps;
const myPropKeys = new Set(/* ??? */);

const coerce = (o: any): MyProps => {
  let p = {};
  for (const k in myProps) {
    if (myPropsOfTypeDate.has(k)) {
      p[k] = new Date(o[k]);
    } else if (myPropsOfTypeBoolean.has(k)) {
      p[k] = o[k] === "true";
    } else {
      p[k] = o[k]
    }
  }
  return p;
}

How can I achieve this (or something comparable) without turning a union type of string literals into a runtime object (Set, array, ...) like suggested in this issue?

krryan commented 2 years ago

@fdcds The typing of your coerce function calls for a mapped type with some conditional typing inside, and the runtime will need some standard JavaScript approaches to detecting Date objects. You definitely do not need the functionality requested here; even if we had it, there are better ways to do what you want.

This isn’t really the place to get into those better ways, but were it me, I would go with this:

type MyJsonObject = {
  [K in keyof MyProps]: MyProps[K] extends Date ? string : MyProps[K];
}

function coerce(props: MyProps): MyJsonObject {
  result = {} as MyJsonObject;
  Object.keys(props).forEach(key => {
    result[key] = props[key] instanceof Date ? props[key].toISOString() : props[key];
  });
  return result;
}

I just woke up, wrote this on my phone, and did not test it. If it doesn’t completely work, it should still be enough to point you in the right directions. Please do not clutter this thread, or even this issue tracker, with questions about it: questions like this belong on Stack Overflow.

Dragon-Hatcher commented 1 year ago

Here is how you can extend @tatemz solution to work for much larger unions by taking advantage of the fact that typescript uses tail-call for certain types of recursive types. This should work for unions of size up to 1000. (Though it gets slow fast.)

type UnionToIntersection<U> = (
  U extends never ? never : (arg: U) => never
) extends (arg: infer I) => void
  ? I
  : never;

type UnionToTuple<T, A extends any[] = []> = UnionToIntersection<
  T extends never ? never : (t: T) => T
> extends (_: never) => infer W
  ? UnionToTuple<Exclude<T, W>, [...A, W]>
  : A;
donaldpipowitch commented 1 year ago

I would like to map a union to a tuple and then use this tuple to map it to an array where every original union type is used once as a key. Is that possible somehow? I tried this, but it fails:

type SomeUnion = 'hello' | 'world';

type SomeTuple = UnionToTuple<SomeUnion>;

type Box<T> = { someField: string; type: T };

type Boxes = {
    [Index in keyof SomeTuple]: Box<SomeTuple[Index]>
} & {length: SomeTuple['length']};

const boxes: Boxes = [
    { someField: '', type: 'hello' },
    { someField: '', type: 'world' },
]

// helper below:
// see https://github.com/microsoft/TypeScript/issues/13298#issuecomment-1610361208
type UnionToIntersection<U> = (U extends never ? never : (arg: U) => never) extends (arg: infer I) => void
  ? I
  : never;
type UnionToTuple<T, A extends any[] = []> = UnionToIntersection<T extends never ? never : (t: T) => T> extends (_: never) => infer W
  ? UnionToTuple<Exclude<T, W>, [...A, W]>
  : A;

link

krryan commented 1 year ago

I would like to map a union to a tuple and then use this tuple to map it to an array where every original union type is used once as a key. Is that possible somehow?

No, it is not, and basically can’t be made to be. Effectively, the problem is that unions do not have order, while arrays (and sets and so on) do. So to properly capture “each possibility of 'hello' | 'world' exactly once,” you need ['hello', 'world'] | ['world', 'hello']. For each additional option in the union, the number of tuples you have to consider grows exponentially.

If you are talking about a union of strings (or numbers, or symbols I think?), there is a solution: you can use an object’s keys as an “order-less” “array” of sorts. (In truth, object keys do have an order defined by JavaScript but Typescript treats reorderings as the same object.)

But for me, for the most part, I have tried to stick to defining the array first, and defining the union based on the array (which is easy). Kind of awkward, not my favorite, I just woke up so I can’t think of any but I feel like there are some limitations, but it mostly works.

Cellule commented 1 year ago

For those using asAllUnionTuple from @Harpush 's solution in TypeScript 5.1 I've noticed that it started breaking in places where the array wasn't const

Adding as const at the call site fixes the problem: asAllUnionTuple<U>()([...] as const) But I've also figured that changing U in asAllUnionTuple has the same effect without having the change the call site

const asAllUnionTuple = <T>() => <const U extends ReadonlyArray<any>>(
  cc: AllUnionTuple<T, U>
) => cc;

The downside is that the resulting tuple is now readonly which can potentially cause other problems in your code

rtritto commented 6 days ago
type MyTypeUnion = MyType1 | MyType2[]

type UnionToTuple = ...

type ConvertedTuple = UnionToTuple<MyTypeUnion>

type FirstType = ConvertedTuple[1]  // MyType1
type SecondType = ConvertedTuple[2]  // MyType2[]

How can I do the UnionToTuple type definition?

krryan commented 6 days ago

How can I do the UnionToTuple type definition?

You can’t, it’s not possible, that’s why this issue is closed. Unions are unordered, tuples are ordered—a tuple requires information that a union hasn’t got. If possible—it isn’t always—you may want to write your definition as an array to begin with, and then tuple-to-union is easy:

export const tuple = [
  // …
] as const;
export type Union = typeof tuple[number];

Unfortunately, this often isn’t workable when your union isn’t hard-coded, but derived from other types.

There is a way you can have the compiler check that your tuple contains all the elements in the union, nothing else, and no duplicates, but it’s a fairly tedious amount of boilerplate:

This bit you can re-use:

/** The unique elements of the input tuple, in order. Only works with `readonly` tuples. */
export type SetTuple<T extends readonly any[]> = _SetTuple<T>;
type _SetTuple<T extends readonly any[], A extends readonly any[] = readonly []> =
    T extends readonly [infer H, ...infer R]
        ? H extends A[number]
            ? _SetTuple<R, A>
            : _SetTuple<R, readonly [...A, H]>
        : A;

Here are your union and tuple definitions, and compile-time testing that the tuples match the union.

import { SetTuple } from 'set-tuple'; // wherever SetTuple is exported from

export type TheUnion = 'foo' | 'bar';

/** A correct tuple, in the same order as the union */
export const validTuple = ['foo', 'bar'] as const;
((): SetTuple<typeof validTuple> => validTuple); // no error: has no duplicates
((): readonly TheUnion[] => validTuple); // no error: does not include anything but 'foo' and 'bar'
((union: TheUnion): typeof validTuple[number] => union); // no error: includes both 'foo' and 'bar'

/** A correct tuple, in the opposite order as the union (doesn't matter) */
export const alsoValidTuple = ['bar', 'foo'] as const;
((): SetTuple<typeof alsoValidTuple> => alsoValidTuple); // no error: has no duplicates
((): readonly TheUnion[] => alsoValidTuple); // no error: does not include anything but 'foo' and 'bar'
((union: TheUnion): typeof alsoValidTuple[number] => union); // no error: includes both 'foo' and 'bar'

/** A wrong tuple, for several reasons */
export const invalidTuple = ['bar', 'bar', 'baz'] as const;
((): SetTuple<typeof invalidTuple> => invalidTuple); // error: duplicate 'bar' elements
((): readonly TheUnion[] => invalidTuple); // error: 'baz' is not in the union
((union: TheUnion): typeof invalidTuple[number] => union); // error: 'foo' is not in the tuple

Here, we use a trio of never-invoked anonymous function expressions, which don’t pollute the namespace and have minimal effect on the run time, and allow us to include some statements that the compiler will check for us, so we can confirm the tuple is what we want it to be. As you can see, you have to include all three statements for each tuple you want to check. (You could combine the first two, technically, but that’s not a lot of savings and it makes the error messages harder to read.) I tried writing a utility type or even utility function that would save on this boilerplate, but everything I came up with had the same two flaws: you still needed a bunch of boilerplate to cause it to actually error when it’s supposed to, and the error messages are impossible to read.

Still, if you only have a few such tuples, and they really must cover the union, this is a safe solution.

yuanhong88 commented 5 days ago
type TuplifyUnion<U extends string> = {
  [S in U]: // for each variant in the union
    Exclude<U, S> extends never // remove it and..
      ? [S] // ..stop recursion if it was the last variant
      : [...TuplifyUnion<Exclude<U, S>>, S] // ..recur if not
}[U] // extract all values from the object

type fs = TuplifyUnion<'1' | '2' | '3'>;
//equal to
type fs = ["3", "2", "1"] | ["2", "3", "1"] | ["3", "1", "2"] | ["1", "3", "2"] | ["2", "1", "3"] | ["1", "2", "3"]

playground

krryan commented 5 days ago

Ok, yes, that works, as long as the union is very small. I tested my implementation with a 114-member union (letters A-Z and a-z, digits 0-9, Greek letters Α-Ω and α-ω, and 'foo', 'bar', and 'baz'), which was no problem. In my testing, TuplifyUnion starts to slow down at 9 union members, and at 10 evaluating it in VS Code Intellisense times out and it’s typed as any (and I’m frankly impressed as hell that Typescript got that far, since the resulting type for 9 was a union of over 300,000 tuples). TuplifyUnion also had significant performance problems that affected my entire editor with larger unions.

I also came up with a less-boilerplate-y way to create a re-usable type to check a tuple covers a union:

export type TestTupleExactlyCoversUnion<T extends readonly any[], U extends string | number | bigint | boolean | null | undefined> =
    [T, U] extends [SetTuple<T> & readonly U[], T[number]] ? true : Split<T extends any ? 'string--------------------------------------------' : never>;
type Split<T> = T extends `${infer H}${infer Rest}` ? readonly [H, ...Split<Rest>] : readonly [];

{
    type Good = TestTupleExactlyCoversUnion<typeof validTuple, TheUnion>;
    type Bad = TestTupleExactlyCoversUnion<typeof invalidTuple, TheUnion>;
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               Type instantiation is excessively deep and possibly infinite. ts(2589)

}

Putting the types in the brackets avoids polluting the namespace, so the names only have to be unique within that context, which is good. You still have to assign names to them, which is annoying. The error message is also much less useful: you just get “Type instantiation is excessively deep and possibly infinite,” which is not terribly helpful (also, if you have a union too large for SetTuple—I didn’t find its limit but I’m pretty sure it’s got one—you’ll get exactly the same error and won’t be able to distinguish “my union is too large” from “my tuple is wrong.” I still think the separate trio of statements is superior, but this does work.