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:
I don't know what ordering is best (or even feasible), but it would have to be nailed down in some predictable form.
Nesting is complicated.
I expect generics would be difficult to support?
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).
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;
.
functionof
would not hurt either: #12265
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)
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.
or just [1,2,3][number] will give you 1 | 2 | 3
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"]
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.
@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.
@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).
@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,
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.
@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?
@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 ...
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]
@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
.
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.
@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. :neutral_face:
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).
@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]
}
}
@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
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.
and source file order is what? đĽđżđ
Quite the tangent from this thread!
you people did it one time, you can do it again, order is order
@RyanCavanaugh OK, a more concrete example. We have a very-large discriminated union that has to get âhandled,â which in this case means there needs to be a particular mapping from that union to another. I wrote a factory to build up these handlers, because we were having a lot of trouble with their size, repetition, and occasional differences here and there that were difficult to track (and more difficult to update).
And, of course, I wanted to test this factory as well as anything built with it. That came with the problem that there was no good way to create the basic test that just runs through every member of the union, to make sure everything is getting âcapturedâ correctly.
This is a heavily abbreviated version of something similar to that system.
// making up a fake discriminated union
declare type FooData;
interface Foo { kind: 'foo'; data: FooData; }
declare const foo: Foo;
declare type BarData;
interface Bar { kind: 'bar'; data: BarData; }
declare const bar: Bar;
declare type BazData;
interface Baz { kind: 'baz'; data: BazData; }
declare const baz: Baz;
type DiscriminatedUnion = Foo | Bar | Baz;
// our test case of just "everything"
const eachThingInTheUnion: tupleof DiscriminatedUnion = [foo, bar, baz];
// the factory is more complex than this, but I believe it gives the idea
interface HandlerFactory<Out> {
// many .handle calls have manually-hard-coded generic arguments
// as inference doesn't work for whatever reason
handle: <SomeIn extends DiscriminatedUnion, NewOut>(handler: (value: SomeIn) => NewOut) => HandlerFactory<Out | NewOut>;
build: () => (value: DiscriminatedUnion) => Out;
}
declare function buildHandler<Out>(defaultHandling: (value: DiscriminatedUnion) => Out): HandlerFactory<Out>;
// Test the handlers built by the factory.
// Notably there are at least a few cases, either due to special circumstances
// or simple legacy code where the handler is NOT built by the factory,
// but is manually created. Still want to be able to test them here.
function testSomeHandler<Out>(
description: string,
handle: (value: DiscriminatedUnion) => Out,
everyOut: tupleof Out,
areOutsEqual: (one: Out, another: Out) => boolean,
): void {
// Jasmine test
describe(description, () => it('Handles all kinds.', () => {
const results = eachThingInTheUnion.map(handle);
results.forEach(result => expect(
everyOut.find(out => areOutsEqual(out, result))
).toNotBe(undefined));
everyOut.forEach(out => expect(
results.find(result => areOutsEqual(out, result))
).toNotBe(undefined));
}));
}
Since all I want to do with the âtuplesâ is iterate over them and then check for the presence of each object in one in the other (and vice versa), the actual order in which they appear doesnât matter to me. I would be equally happy to have tupleof
not produce a tuple at all, as long as it produces a collection I can iterate over and search through that is guaranteed to have one (ideally, exactly one) of each thing in the union.
guaranteed to have one (ideally, exactly one) of each thing in the union
This is a bit awkward, but isn't order-dependent:
declare type FooData = number;
interface Foo { kind: 'foo'; data: FooData; }
declare const foo: Foo;
declare type BarData = string;
interface Bar { kind: 'bar'; data: BarData; }
declare const bar: Bar;
declare type BazData = boolean;
interface Baz { kind: 'baz'; data: BazData; }
declare const baz: Baz;
type DiscriminatedUnion = Foo | Bar | Baz;
type ExactMatch<Union, Arr> = Union[] extends Arr ? Arr extends Union[] ? Arr : never : never;
function testHasAll<T>() {
return function <U>(arg: ExactMatch<T, U>) {
}
}
// OK
const MyGoodArr = [foo, bar, baz];
testHasAll<DiscriminatedUnion>()(MyGoodArr);
const MyBadArr = [foo, baz];
// Error
testHasAll<DiscriminatedUnion>()(MyBadArr);
@RyanCavanaugh
@treybrisbane the order is not guaranteed.
What do you think of this?
Yeah, what you did there is basically what I've already done. đ
I think that if the ES spec doesn't guarantee ordering of object entries (which is fair enough), then I consider converting an object type to an in-order tuple of entries to be a fundamentally invalid operation.
I clearly need to find a different approach!
For what it's worth, here's an (arguably) simpler version of what @ShanonJackson posted above:
type Head<T extends any[]> = T extends [infer U, ...unknown[]] ? U : never
type Tail<T extends any[]> =
T extends any[] ? ((...args: T) => never) extends ((a: any, ...args: infer R) => never) ? R : never : never
type Cons<T extends any[], H> = ((h: H, ...t: T) => any) extends ((...r: infer R) => any) ? R : never;
interface Reduction<Base, In> {
0: Base
1: In
}
type UnionReduce<U, R extends Reduction<any, any>> = R[[U] extends [never] ? 0 : 1];
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type UnionToFunctionOverloads<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;
type UnionPop<U> = UnionToFunctionOverloads<U> extends ((a: infer A) => void) ? A : never;
interface UnionToTupleRec<T extends any[], U>
extends Reduction<T, UnionReduce<Exclude<U, UnionPop<U>>, UnionToTupleRec<Cons<T, UnionPop<U>>, Exclude<U, UnionPop<U>>>>> { }
type UnionToTuple<U> =
UnionReduce<U, UnionToTupleRec<[], U>> extends infer T ? T extends any[] ? T : never : never;
// -----------------------------------------------------------------------------
interface Person {
firstName: string;
lastName: string;
dob: Date;
hasCats: false;
}
const keysOfPerson: UnionToTuple<keyof Person> = ["firstName", "lastName", "dob", "hasCats"];
It basically works by (ab)using the fact that type-level pattern matching will only infer the type of the last overload when given a set of function overloads.
It's probably not a behavior we should be relying on, tbh. đ
@RyanCavanaugh Appreciated, that is usefulâbut as you say, it is pretty awkward. I can even more it purely to the type domain:
const testGood = [foo, bar, baz];
const testedGood: ExactMatch<DiscriminatedUnion, typeof testGood> = testGood;
const testBad = [foo, bar];
const testedBad: ExactMatch<DiscriminatedUnion, typeof testBad> = testBad;
but this is still pretty awkward.
Anyway, another thing that occurs to me that would improve this work-around, or (probably) _any_ workaround, would be a way to get the number of options in a union. This gets awkward in the case of union members that arenât mutually exclusive (e.g. type A = { foo: number; }; type B = A & { bar: number; }; type U = A | B;
âshould the result here be 1 or 2? unclear.), but I think defining such a thing as being the number of _mutually-exclusive_ options in a union would be the right call (so the answer for U
would be 1). That can probably be added on to my large âitâd be great if Typescript recognized when all members of a union are mutually exclusive and leveraged that factâ wishlist, though, I guess.
Anyway, another thing that occurs to me that would improve this work-around, or (probably) any workaround, would be a way to get the number of options in a union.
If the above UnionToTuple
implementations work (for string literals?), get the tuple then get the length off that using ['length']
? Or did you need the length first?
I'm just trying to paste these snippets into TS Playground now, but it complains about everything so I guess it must be quite behind. :neutral_face:
@tycho01 I tested my above code on the official TS playground. Tried again just now and can confirm that it works. đ
I forgot why I couldn't just use mapped types, but here's a failed attempt to mimic mapped types by iteration with the above UnionToTuple
, using the operation T
-> Array<T>
as a toy example (where I'd really like to have a function type call instead):
type NumberToString = { [i: number]: string; 0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 13: '13', 14: '14', 15: '15', 16: '16', 17: '17', 18: '18', 19: '19', 20: '20', 21: '21', 22: '22', 23: '23', 24: '24', 25: '25', 26: '26', 27: '27', 28: '28', 29: '29', 30: '30', 31: '31', 32: '32', 33: '33', 34: '34', 35: '35', 36: '36', 37: '37', 38: '38', 39: '39', 40: '40', 41: '41', 42: '42', 43: '43', 44: '44', 45: '45', 46: '46', 47: '47', 48: '48', 49: '49', 50: '50', 51: '51', 52: '52', 53: '53', 54: '54', 55: '55', 56: '56', 57: '57', 58: '58', 59: '59', 60: '60', 61: '61', 62: '62', 63: '63', 64: '64', 65: '65', 66: '66', 67: '67', 68: '68', 69: '69', 70: '70', 71: '71', 72: '72', 73: '73', 74: '74', 75: '75', 76: '76', 77: '77', 78: '78', 79: '79', 80: '80', 81: '81', 82: '82', 83: '83', 84: '84', 85: '85', 86: '86', 87: '87', 88: '88', 89: '89', 90: '90', 91: '91', 92: '92', 93: '93', 94: '94', 95: '95', 96: '96', 97: '97', 98: '98', 99: '99', 100: '100', 101: '101', 102: '102', 103: '103', 104: '104', 105: '105', 106: '106', 107: '107', 108: '108', 109: '109', 110: '110', 111: '111', 112: '112', 113: '113', 114: '114', 115: '115', 116: '116', 117: '117', 118: '118', 119: '119', 120: '120', 121: '121', 122: '122', 123: '123', 124: '124', 125: '125', 126: '126', 127: '127', 128: '128', 129: '129', 130: '130', 131: '131', 132: '132', 133: '133', 134: '134', 135: '135', 136: '136', 137: '137', 138: '138', 139: '139', 140: '140', 141: '141', 142: '142', 143: '143', 144: '144', 145: '145', 146: '146', 147: '147', 148: '148', 149: '149', 150: '150', 151: '151', 152: '152', 153: '153', 154: '154', 155: '155', 156: '156', 157: '157', 158: '158', 159: '159', 160: '160', 161: '161', 162: '162', 163: '163', 164: '164', 165: '165', 166: '166', 167: '167', 168: '168', 169: '169', 170: '170', 171: '171', 172: '172', 173: '173', 174: '174', 175: '175', 176: '176', 177: '177', 178: '178', 179: '179', 180: '180', 181: '181', 182: '182', 183: '183', 184: '184', 185: '185', 186: '186', 187: '187', 188: '188', 189: '189', 190: '190', 191: '191', 192: '192', 193: '193', 194: '194', 195: '195', 196: '196', 197: '197', 198: '198', 199: '199', 200: '200', 201: '201', 202: '202', 203: '203', 204: '204', 205: '205', 206: '206', 207: '207', 208: '208', 209: '209', 210: '210', 211: '211', 212: '212', 213: '213', 214: '214', 215: '215', 216: '216', 217: '217', 218: '218', 219: '219', 220: '220', 221: '221', 222: '222', 223: '223', 224: '224', 225: '225', 226: '226', 227: '227', 228: '228', 229: '229', 230: '230', 231: '231', 232: '232', 233: '233', 234: '234', 235: '235', 236: '236', 237: '237', 238: '238', 239: '239', 240: '240', 241: '241', 242: '242', 243: '243', 244: '244', 245: '245', 246: '246', 247: '247', 248: '248', 249: '249', 250: '250', 251: '251', 252: '252', 253: '253', 254: '254', 255: '255' };
type Inc = { [i: number]: number; 0: 1; 1: 2; 2: 3; 3: 4; 4: 5; 5: 6; 6: 7; 7: 8; 8: 9; 9: 10 };
type Dec = { [i: number]: number; 0: -1; 1: 0; 2: 1; 3: 2; 4: 3; 5: 4; 6: 5; 7: 6; 8: 7; 9: 8 };
type TupleHasIndex<
Arr extends ArrayLike<any>,
I extends number
> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
type UnionHasKey<Union extends string, K extends string> = ({[S in Union]: '1' } & {[k: string]: '0'})[K];
type StringsEqual<
A extends string,
B extends string
> = UnionHasKey<A, B>;
type ArrayifyObject<
Obj,
Tpl extends ReadonlyArray<any> = UnionToTuple<keyof Obj>,
Len = Tpl['length'],
I extends number = 0,
Acc = {},
> = {
0: Acc,
1: ArrayifyObject<
Obj,
Tpl,
Inc[I],
Acc & { [P in Tpl[NumberToString[I]]]: Array<Obj[P]> }
>
// }[TupleHasIndex<Tpl, I>];
}[StringsEqual<Len, I>];
It yields me some ... cannot be used to index type ...
. I dunno how iteration is done anymore these days. :)
đ i hope it's just an academic exercise and no one is desperate that bad to use either of these code monsters
I use these "code monsters" all the time, code type-safety to me is more important than type readability.
@RyanCavanaugh I happened to find this Stack Overflow answer today, which seems to suggest that while for-in
and Object.keys
don't follow a well-defined order for keys, other operations like Object.getOwnPropertyNames
, Object.getOwnPropertySymbols
, and Reflect.ownKeys
_do_ follow a well-defined order.
Not sure if its worth adding an entire feature for just those operations, but I figure it's relevant enough to post here anyway. đ
One problem I have is that in React, you often pass-through props with spread syntax, like <Foo {...this.props}/>
. However, this can result in a lot of props being forwarded that are actually not on Foo
. If Foo
is a PureComponent
, it will do a comparison of the props with the previous props to determine if it should rerender. But if more props are passed than used, more props are compared and the comparison takes longer. If you pass props like this through every component _every_ component will rerender.
The solution to this would be to implement shouldComponentUpdate()
, and making sure only actual props are compared:
interface Props {
a: any
b: any
}
const propKeys = ['a', 'b']
class Foo extends Component<Props> {
shouldComponentUpdate(props: Props, prevProps: Props): boolean {
return isEqual(pick(props, propKeys), pick(prevProps, propKeys))
}
}
But it is impossible right now to enforce that all props (no more, no less) are compared. It's easy for a dev to add a new prop and forget to add it to shouldComponentUpdate()
.
I wish I could use something like:
return arePropsEqual(props, prevProps, ['a', 'b'] as const)
which would be defined as:
const arePropsEqual = <P extends object>(props: P, otherProps: P, keys: TupleOf<keyof P>) =>
isEqual(pick(props, keys), pick(otherProps, keys))
whenever a prop is added or removed, TypeScript would ensure the array is updated too.
there is a way:
const lessProps: LessProps = {...moreProps}; // <-- this should catch it
<MyPureComponent {...lessProps} />
@aleksey-bykov you would have to do that for every single reference to a component, and for every single component in your render(). And there is nothing enforcing that it's done
yes
@felixfbecker you can do this and actually get remarkably good error messages. Version that can be polished a bit:
interface Props {
a: any
b: any
}
type Values<T> = T extends { [index: number]: infer E } ? E : never;
type Extra<Desired, Actual> = Exclude<Actual, Desired> extends never ? true : { "extra": Exclude<Actual, Desired> };
type Missing<Desired, Actual> = Exclude<Desired, Actual> extends never ? true : { "missing": Exclude<Desired, Actual> };
type IsCorrect<Props, Array> = Extra<keyof Props, Values<Array>> | Missing<keyof Props, Values<Array>>;
function checkProps<T, U>(props: T, names: U): IsCorrect<T, U> {
return true as any;
}
function assertTrue(t: true) {}
declare const props: Props;
// OK
assertTrue(checkProps(props, ["a", "b"] as const));
// Error
assertTrue(checkProps(props, ["a", "b", "c"] as const));
// Error
assertTrue(checkProps(props, ["a", "c"] as const));
// Error
assertTrue(checkProps(props, ["a"] as const));
That looks awesome! I am struggling a bit with translating that into the arePropsEqual
signature (i.e. having a constraint on the keys
parameter that it must be an exhaustive Array<keyof P>
). Is that possible?
am i the only one who sees an all so growing need for type domain assertions? (and lack of thereof support from the language?)
problem is:
MustBeCorrect<Props, Array>; // <-- syntax errror
only
type _ThankGodThisThingWasIndeedCorrect = MustBeCorrect<Props, Array>;
there is no better way to do so, other than using phantom functions like assertTrue
there is no wrong
type:
type X<T> = T extends Whatever ? true : wrong<`You can't be here, because...`>
i am looking at you @RyanCavanaugh
Here's a fully-evaporating version with only one name leak. I think you could generalize this into a checkable OK/not-OK thing.
interface Props {
a: any
b: any
}
type Values<T> = T extends { [index: number]: infer E } ? E : never;
type Extra<Desired, Actual> = Exclude<Actual, Desired> extends never ? { ok: true } : { "extra": Exclude<Actual, Desired> };
type Missing<Desired, Actual> = Exclude<Desired, Actual> extends never ? { ok: true } : { "missing": Exclude<Desired, Actual> };
type IsCorrect<Props, Array> = Extra<keyof Props, Values<Array>> | Missing<keyof Props, Values<Array>>;
const goodProps = ["a", "b"] as const;
const badProps = ["a", "c"] as const;
namespace PropsCheck {
type Check1 = IsCorrect<Props, typeof goodProps>["ok"];
type Check2 = IsCorrect<Props, typeof badProps>["ok"];
}
That example returns them as if they were legitimate return types though right? I think I can see the use-case @aleksey-bykov is getting at.
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.....
Where's the reaction for "Please don't put that in your project" ? đ
Lockpicking with types:
// Secret number sequence, do not share
type CombinationLock<T> = T extends [5, 6, 2, 4, 3, 1] ? T : never;
// Sneaky sneak
type Obj = { 1, 2, 3, 4, 5, 6 }
export const k: UnionToTuple<keyof Obj> = null as any;
// Mouseover on q
export const q: CombinationLock<typeof k> = null as any;
this makes my eyes bleed, i am calling the cops
See comments above - this is not happening.
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.
@RyanCavanaugh how do I type the string keys of an enum in enum order?
I'm using an enum's keys to dictate object properties and valid values all over the place, I want the abillity to unroll to function arguments as well (in order, with one property name in the enum having different values to the others):
Example as asked:
export enum FloorLayers {
Natural,
Foundation,
Insulation,
Misc,
Networks,
Cover
}
function (natural: Ground | null, foundation: Floor | null, insulation: Floor | null,
misc: Floor | null, networks?: Network | null, cover: Floor | null) {
I want the ability to add to my enum and have the function's arguments update without having to rewrite the signature, because this will actually be happening a lot for a lot of different functions, to do this I want to store the enum types in a tuple in the same file as the enum, and then when declaring functions I can assign types to them based on whether or not they extend "Natural"
or "Networks"
that differ to the rest of the types. I can then access the arguments using the order in the enum (because the order is preserved).
All of the tuple types presented above that preserve order are erroring about potentially infinite recursion except fighting cat but I only want one tuple, not all permutations. I'm using VS TS 3.4.
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.....
Could you provider any explanation?
For example, I can understand :
type UnionToTupleRecursively_<T extends any[], U, S> =
S extends any ? UnionToTupleRecursively<TupleUnshift<T, S> | TuplePush<T, S>, Exclude<U, S>> : never;
Why S extends any
is required in there?
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.
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.....
@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;
@hediet But there is no use of destructuring in this demonstration...
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.
@Griffork The problem here is multi-fold and has been explained in this thread, some highlights:
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.
I wish I could đ @dragomirtitian's comment more than once
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
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>
Don't try it with 10 elements.
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
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.
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.
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"];
(Something, something, probably don't use this in your projects, something)
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);
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;
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.
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"];
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 ... | [...]
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.
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"]
There's no inherent ordering of properties, so this will break if you look at it funny. Please just don't đ
@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;
(Requires nightly until 4.1 lands)
(This is academic; don't ever use this)
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"]
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.
Most helpful comment
You can already do
tuple -> union
conversion:How about a concat operator for
union -> tuple
conversion?Are there good use cases for preserving union order? (while still treating unions as sets for comparison purposes)