Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type manipulations: union to tuple #13298

Closed
krryan opened this issue Jan 5, 2017 · 81 comments
Closed

Type manipulations: union to tuple #13298

krryan opened this issue Jan 5, 2017 · 81 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@krryan
Copy link

krryan commented Jan 5, 2017

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;.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 5, 2017

functionof would not hurt either: #12265

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@aleclarson
Copy link

aleclarson commented Sep 21, 2018

You can already do tuple -> union conversion:

[3, 1, 2][number] // => 1 | 2 | 3

type U<T extends any[], U = never> = T[number] | U
U<[3, 1, 2]> // => 1 | 2 | 3
U<[1], 2 | 3> // => 1 | 2 | 3

How about a concat operator for union -> tuple conversion?

type U = 1 | 2 | 3
type T = [0] + U        // => [0, 1, 2, 3]
type S = U + [0]        // => [1, 2, 3, 0]
type R = [1] + [2]      // => [1, 2]
type Q = R + R          // => [1, 2, 1, 2]
type P = U + U          // Error: cannot use concat operator without >=1 tuple
type O = [] + U + U     // => [1, 2, 3, 1, 2, 3]
type N = [0] + any[]    // => any[]
type M = [0] + string[] // Error: type '0' is not compatible with 'string'
type L = 'a' + 16 + 'z' // => 'a16z'

Are there good use cases for preserving union order? (while still treating unions as sets for comparison purposes)

@krryan
Copy link
Author

krryan commented Sep 21, 2018

I had used a conditional type for tuple to union:

type ElementOf<T> = T extends (infer E)[] ? E : T;

Works for both arrays and tuples.

@ShanonJackson
Copy link

ShanonJackson commented Feb 27, 2019

or just [1,2,3][number] will give you 1 | 2 | 3

@ShanonJackson
Copy link

ShanonJackson commented Feb 28, 2019

Decided to stop being a lurker and start joining in the Typescript community alittle more hopefully this contribution helps put this Union -> Tuple problem to rest untill Typescript hopefully gives us some syntax sugar.

This is my "N" depth Union -> Tuple Converter that maintains the order of the Union

// add an element to the end of a tuple
type Push<L extends any[], T> =
  ((r: any, ...x: L) => void) extends ((...x: infer L2) => void) ?
    { [K in keyof L2]-?: K extends keyof L ? L[K] : T } : never
  
export type Prepend<Tuple extends any[], Addend> = ((_: Addend, ..._1: Tuple) => any) extends ((
	..._: infer Result
) => any)
	? Result
	: never;
//
export type Reverse<Tuple extends any[], Prefix extends any[] = []> = {
	0: Prefix;
	1: ((..._: Tuple) => any) extends ((_: infer First, ..._1: infer Next) => any)
		? Reverse<Next, Prepend<Prefix, First>>
		: never;
}[Tuple extends [any, ...any[]] ? 1 : 0];



// convert a union to an intersection: X | Y | Z ==> X & Y & Z
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

// convert a union to an overloaded function X | Y ==> ((x: X)=>void) & ((y:Y)=>void)     
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;

// returns true if the type is a union otherwise false
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

// takes last from union
type PopUnion<U> = UnionToOvlds<U> extends ((a: infer A) => void) ? A : never;

// takes random key from object
type PluckFirst<T extends object> = PopUnion<keyof T> extends infer SELF ? SELF extends keyof T ? T[SELF] : never;
type ObjectTuple<T, RES extends any[]> = IsUnion<keyof T> extends true ? {
    [K in keyof T]: ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>> extends any[]
        ? ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>>
        : PluckFirst<ObjectTuple<Record<Exclude<keyof T, K>, never>, Push<RES, K>>>
} : Push<RES, keyof T>;

/** END IMPLEMENTATION  */



type TupleOf<T extends string> = Reverse<PluckFirst<ObjectTuple<Record<T, never>, []>>>

interface Person {
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;
}
type Test = TupleOf<keyof Person> // ["firstName", "lastName", "dob", "hasCats"]

@krryan krryan changed the title Type manipulations: union to tuple, tuple to union Type manipulations: union to tuple Feb 28, 2019
@krryan
Copy link
Author

krryan commented Feb 28, 2019

Finally removed the bit about union to tuple, since there are plenty of ways to do that now (there weren’t when this suggestion was first made). Also, much thanks to @ShanonJackson, that looks awesome and I will have to try that. Still, that’s a lot of code for this; sugar would be rather appreciated here. Or at least a built-in type that comes with Typescript, so that doesn’t have to be re-implemented in every project.

@aleclarson
Copy link

aleclarson commented Feb 28, 2019

@krryan A solution of that size should be published as an NPM package, IMO.

Worth noting: The TupleOf type provided by @ShanonJackson only supports string unions, so it's not a universal solution by any means.

@krryan
Copy link
Author

krryan commented Feb 28, 2019

@aleclarson Yes, but installing a dependency is, to my mind, still “reimplementing” it, at least in the context here. Sure, an NPM package is superior to copying and pasting that code around. But I don’t think either should be necessary for this. It’s a language construct that is broadly useful to all Typescript developers, in my opinion, so it should just be available (and quite possibly be implemented more easily within tsc than as a type in a library).

Anyway, good point about the string limitation; that’s quite severe (I might still be able to use that but it’s going to take some work since I’ll have to get a tuple of my discriminants and then distribute those appropriately, but I think it will work for my purposes).

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Feb 28, 2019

@ShanonJackson

I fear that while this solution works, it is very compiler unfriendly .. I added just a couple more keys to the object and when I hovered over it the language server got up to 100% CPU usage, ate up 3GB of RAM and no tooltips ever show up.

interface Person {
    firstName: string;
    lastName: string;
    dob: Date;
    hasCats: false;
    hasCats1: false;
    hasCats2: false;
    hasCats3: false;
    hasCats4: false;
}
type Test = TupleOf<keyof Person> //  tool tip never shows up HUGE amount of RAM and CPU Used,

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs More Info The issue still hasn't been fully clarified and removed Needs Investigation This issue needs a team member to investigate its status. labels Feb 28, 2019
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Feb 28, 2019

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.

Secondary comments: This suggestion could almost certainly never happen; problems with it are many.

First, union order is not something we can ever allow to be observable. Internally, unions are stored as a sorted list of types (this is the only efficient way to quickly determine relationships between them), and the sort key is an internal ID that's generated incrementally. The practical upshot of this is that two extremely similar programs can generate vastly different union orderings, and the same union observed in a language service context might have a different ordering than when observed in a commandline context, because the order in which types are created is simply the order in which they are checked.

Second, there are basic identities which are very confusing to reason about. Is tupleof T | ( U | V ) [T, U | V] or [T, U, V] ? What about this?

// K always has arity 2?
type K<T, U> = tupleof (T | U);
// Or Q has arity 3? Eh?
type Q = K<string, number | boolean>;

There are more problems but the first is immediately fatal IMO.

@krryan
Copy link
Author

krryan commented Feb 28, 2019

@RyanCavanaugh The primary use-case for me is to ensure complete coverage of all the types that a function claims to be able to handle in testing scenarios. There is no way to generate an array you can be sure (tsc will check) has every option.

Order doesn’t matter to me at all, which makes it frustrating to have that as a fatal flaw. I think Typescript programmers are already familiar with union ordering being non-deterministic, and that’s never really been a problem. I wonder if creating something typed as Set<MyUnion> but with a fixed size (i.e. equal to the number of members of MyUnion) would be more valid? Sets are ordered, but that would be at runtime, rather than exposed as part of its type, which maybe makes it acceptable (since it’s not part of the type, looking at the code you have no reason to expect any particular order).

As for T | ( U | V ) I would definitely want that to be [T, U, V]. On K and Q, those results (arity 2, arity 3) don’t seem surprising to me and seem quite acceptable. I’m maybe not seeing the issue you’re getting at?

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Feb 28, 2019

@krryan Yes but the problem is that if 'A' | 'B' gets transformed to ['A', 'B'] it should always be transformed to ['A', 'B']. Otherwise you will get random errors at invocation site. I think the point @RyanCavanaugh is making is that this order cannot be guaranteed 100% of the time and may depend on the order and the sort key which is "... an internal ID that generated incrementally"

type  A = "A"
type  B = "B"

type AB = A | B
function tuple(t: tupleof AB) {}

tuple(['A', 'B'])// Change the order in which A and B are declared and this becomes invalid .. very brittle ...

@ShanonJackson
Copy link

ShanonJackson commented Feb 28, 2019

Yes tuples have a strict order unless you write a implementation that can turn [A, B] into [A, B] | [B, A] (permutations). However such a type-level implementation would also be very heavy on the compiler without syntax sugar as when you get up to 9! you get into some ridiculous amount of computation that a recursive strategy will struggle.

If people do care about the order (i don't) then i think just write a implementation that turns [A, B] into...
[A | B, A | B] intersected with a type that makes sure both A & B are present? therefore you can't go [A, A] and also can't go [A, A, B] but can go [A, B] or [B, A]

@krryan
Copy link
Author

krryan commented Feb 28, 2019

@dragomirtitian I fully understand that, which is why I suggested some alternative type that isn’t a tuple type to indicate that we are talking about an unordered set of exactly one each of every member of a union.

Which it now dawns on me can be accomplished for strings by creating a type that uses every string in a union of strings as the properties of a type. For example:

const tuple = <T extends unknown[]>(...a: T): T => a;

type ElementOf<T> = T extends Array<infer E> ? E : T extends ReadonlyArray<infer E> ? E : never;
type AreIdentical<A, B> = [A, B] extends [B, A] ? true : false;

type ObjectWithEveryMemberAsKeys<U extends string> = {
    [K in U]: true;
};

const assertTupleContainsEvery = <Union extends string>() =>
    <Tuple extends string[]>(
        tuple: Tuple,
    ) =>
        tuple as AreIdentical<
            ObjectWithEveryMemberAsKeys<Union>,
            ObjectWithEveryMemberAsKeys<ElementOf<Tuple>>
        > extends true ? Tuple : never;

const foo = 'foo' as const;
const bar = 'bar' as const;
const baz = 'baz' as const;
const assertContainsFooBar = assertTupleContainsEvery<typeof foo | typeof bar>();
const testFooBar = assertContainsFooBar(tuple(foo, bar)); // correctly ['foo', 'bar']
const testBarFoo = assertContainsFooBar(tuple(bar, foo)); // correctly ['bar', 'foo']
const testFoo = assertContainsFooBar(tuple(foo)); // correctly never
const testFooBarBaz = assertContainsFooBar(tuple(foo, bar, baz)); // correctly never
const testFooBarBar = assertContainsFooBar(tuple(foo, bar, bar)); // incorrectly ['foo', 'bar', 'bar']; should be never

There’s probably a way to fix the foo, bar, bar case, and in any event that’s the most minor failure mode. Another obvious improvement is to change never to something that would hint at what’s missing/extra, for example

        > extends true ? Tuple : {
            missing: Exclude<Union, ElementOf<Tuple>>;
            extra: Exclude<ElementOf<Tuple>, Union>;
        };

though that potentially has the problem of a user thinking it’s not an error report but actually what the function returns, and trying to use .missing or .extra (consider this another plug for #23689).

This works for strings (and does not have the compiler problems that the suggestion by @ShanonJackson has), but doesn’t help non-string unions. Also, for that matter, my real-life use-case rather than Foo, Bar, Baz is getting string for ElementOf<Tuple> even though on hover the generic inferred for Tuple is in fact the tuple and not string[], which makes me wonder if TS is shorting out after some number of strings and just calling it a day with string.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Feb 28, 2019

The primary use-case for me is to ensure complete coverage of all the types that a function claims to be able to handle in testing scenarios

How do tuples, as opposed to unions, help with this? I'm begging y'all, someone please provide a hypothetical code sample here for what you'd do with this feature so I can understand why 36 people upvoted it 😅

Order doesn’t matter to me

I can accept this at face value, but you have to recognize that it'd be a never-ending source of "bug" reports like this. The feature just looks broken out of the gate:

type NS = tupleof number | string;
// Bug: This is *randomly* accepted or an error, depending on factors which
// can't even be explained without attaching a debugger to tsc
const n: NS = [10, ""];

I question whether it's even a tuple per se if you're not intending to test assignability to/from some array literal.

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Feb 28, 2019

@RyanCavanaugh:

Is tupleof T | ( U | V ) [T, U | V] or [T, U, V] ?

I'm with @krryan -- only the latter makes sense here. The former would seem quite arbitrary.

union order is not something we can ever allow to be observable.

This is perfectly, as this is not a blocker to its use-cases.

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.

One big problem I see this as solving is map functions on objects (Lodash's mapValues, Ramda's map), which this would allow accurately typing even for heterogeneous objects (-> calculating value types for each key), i.e. what's solved by Flow's $ObjMap, though this implies getting object type's keys, converting them to a union, then converting this union to a tuple type, then using type-level iteration through this tuple using recursive types so as to accumulate value types for each key.

TupleOf may let us do this today. I don't expect this to be a supported use-case of TypeScript. Going through this to type one function may sound silly. But I think it's kind of big.

Anyone who has used Angular's state management library ngrx will be aware that getting type-safe state management for their front-end application involves horrific amounts of boilerplate. And in plain JavaScript, it has always been easy to imagine an alternative that is DRY.

Type-safe map over heterogeneous objects addresses this for TypeScript, by allowing granular types to propagate without requiring massive amounts of boilerplate, as it lets you separate logic (functions) from content (well-typed objects).

edit: I think this depends on the boogieman $Call as well. 😐

@treybrisbane
Copy link

treybrisbane commented Mar 1, 2019

I basically just want to be able to do this:

const objFields: [['foo', 3], ['bar', true]] = entries({ foo: 3, bar: true });

I'm not sure whether the ES spec guarantees ordering of object entries or not. Node's implementation seems to, but if the spec doesn't, then this may just not be something TypeScript should facilitate (since it would be assuming a specific runtime).

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 1, 2019

@treybrisbane the order is not guaranteed.

What do you think of this?

type Entries<K extends object> = {
    [Key in keyof K]: [Key, K[Key]]
};
function entries<K extends object>(obj: K): Entries<K>[keyof K][] {
    return Object.keys(obj).map(k => [k, obj[k]]) as any;
}

const objFields = entries({ foo: 3, bar: "x" });
for (const f of objFields) {
    if (f[0] === "foo") {
        console.log(f[1].toFixed());
    } else if (f[0] === "bar") {
        console.log(f[1].toLowerCase());
    } else {
        // Only typechecks if f[0] is exhausted
        const n: never = f[1]
    }
}

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 1, 2019

@RyanCavanaugh when you say things about how order matters it makes me smile, please tell me where in the spec of typescript can i read about the order of overloads on the same method of the same interface coming from different *.d.ts files please, thank you

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 1, 2019

Each overload is in the order that it appears in each declaration, but the ordering of the declarations is backwards of the source file order. That's it.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 1, 2019

and source file order is what? 🎥🍿😎

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Mar 1, 2019

Quite the tangent from this thread!

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 1, 2019

you people did it one time, you can do it again, order is order

@treybrisbane
Copy link

treybrisbane commented Oct 19, 2019

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
Copy link

Harpush commented Mar 7, 2020

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
Copy link

karol-majewski commented Jul 7, 2020

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
Copy link

treybrisbane commented Aug 18, 2020

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
Copy link
Contributor

pushkine commented Sep 15, 2020

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
Copy link

robarchibald commented Oct 10, 2020

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
Copy link
Contributor

tjjfvi commented Oct 12, 2020

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
Copy link
Member

RyanCavanaugh commented Oct 12, 2020

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

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 12, 2020

@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
Copy link

jo32 commented Nov 10, 2020

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.

@joaopaulobdac
Copy link

joaopaulobdac commented Dec 18, 2020

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
Copy link

ThreePalmTrees commented Apr 15, 2021

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
Copy link

vojtechhabarta commented Apr 20, 2021

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
Copy link

Roaders commented Apr 29, 2021

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
Copy link

eugene-kim commented May 6, 2021

@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
Copy link

tatemz commented Jul 24, 2021

@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
Copy link

jasonkuhrt commented Sep 18, 2021

@tatemz with that I get this error:

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

TS 4.4.2

@HunterKohler
Copy link

HunterKohler commented Sep 30, 2021

@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
Copy link

tatemz commented Sep 30, 2021

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
Copy link

fdcds commented Nov 25, 2021

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
Copy link
Author

krryan commented Nov 25, 2021

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests