Description
Answer Playground Link (TypeScript 4.1+ only)
type Permutation<T, K=T> =
[T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never
type Permuted = Permutation<'a' | 'b'> // ['a', 'b'] | ['b' | 'a']
Whoa...that is weird looking. Don't worry I'll break it all down; weird piece, by weird piece 😆
TLDR by @mistlog (Click me)
Excellent Chinese translation by @zhaoyao91 (Click me)
Explanation
[T] extends [never]
What in the bowels of 16-bit hell is that? Glad you asked. That my friends is a "feature" of TypeScript. It makes sense eventually, so just bear with me.
Imagine you want to make a function called: assertNever
. This function would be used for checking to make sure a type is...well...never
. Pretty simple concept. You might want to use it to check some weird types you are building or something. Just roll with it, OK?
Wanna know a juicy secret? (Click me)
I totally stole the idea of the example I'm explaining here
Anywho, here is what we might create on our first pass:
function assertNever<T>(value: T extends never ? true : false) {}
Cool, we've got something. Let's give it a whirl:
assertNever<string>(true)
// ^^^^ TS Error (2345)
// Argument of type 'true' is not assignable to parameter of type 'false'.
Nice, just what we wanted. This should throw an error because string
isn't assignable to never
. Why does "string
not being assignable to never
" mean we get an error? Because the expected type of the value
param in assertNever
will be false
when T extends never
is false and string extends never
is false. Since we always pass true
to the function, we get an error just like we wanted.
assertNever<never>(true) // this should compile fine. never should extend never, right?
Uh oh, it doesn't work right. We are getting an error here too...weird. But this error is kinda funky...
assertNever<never>(true)
// ^^^^ TS Error (2345)
// Argument of type 'boolean' is not assignable to parameter of type 'never'.
"boolean
is not assignable to parameter of type never
"? But...the parameter should only be true | false
right? If T extends never
it should be true
and if T extends never
is not the case, it should be false
, right?
Well, it turns out T extends never
doesn't work when T = never
but not because of anything to do with the conditional. TypeScript does an interesting thing when unpacking generics into conditionals: it distributes them.
Minor Primer on Distributive Conditional Types (Click me)
Basically, TypeScript sees this:
T extends U ? X : Y
and when provided with a type argument whereT = 'A' | 'B'
it gets "distributed" and resolved as(A extends U ? X : Y) | (B extends U ? X : Y)
.
So let's tie this back into distributing over never
. TypeScript does recursive distribution over type unions. Also note that there is no such thing as a union of unions, a union of unions is just a bigger union with all the elements of all unions in it...
Anyway, the meat of this is: TypeScript treats never
as an empty union when distributing over conditionals. This means that 'a' | never
when getting distributed just gets shortened to 'a'
when distributing. This also means 'a' | (never | 'b') | (never | never)
just becomes 'a' | 'b'
when distributing, because the never
part is equivalent to an empty union and we can just combine all the unions.
So bringing it all in, TypeScript simply ignores empty unions when distributing over a conditional. Makes sense right? Why distribute over a conditional when there is nothing to distribute over?
Now that we know that, we know T extends never
as a conditional is NEVER going to work (pun intended). So how do we tell TypeScript NOT to look at never
as an empty union? Well, we can force TypeScript to evaluate T
before trying to distribute it. This means we need to mutate the T
type in the conditional so the never
value of T
gets captured and isn't lost. We do this because we can't distribute over an empty union (read never
) type for T
.
There are a few easy ways to do this, fortunately! One of them is to just slap T
into a tuple: [T]
. That's probably the easiest. Another one is to make an array of T
: T[]
. Both examples work and will "evaluate" T
into something other than never
before it tries to distribute over the conditional. Here are working examples of both methods (playground link):
function assertNeverArray<T>(value: T[] extends never[] ? true : false) {}
function assertNeverTuple<T>(value: [T] extends [never] ? true : false) {}
// both of these fail, as expected
assertNeverArray<string>(true)
assertNeverTuple<string>(true)
// both of these pass, as expected
assertNeverArray<never>(true)
assertNeverTuple<never>(true)
Phew, finally done with the first line...
Now that you're back from crying in the bathroom...
K extends K
Oh, boy...what the heck is this? This is even WEIRDER than the other one.
Alas! We are now armed with knowledge. Think about it...let's see if you can guess why this is here...I'll give you a hint: what happens to unions in a conditional type?
The answer... (Click me)
K extends K
is obviously always going to be true right? So why even have it in a conditional in the first place? I mean, sure, type unions get distributed over conditionals, but we also know that...wait!?!? "Type unions get distributed over conditionals", that's it!This is a cheeky hack to make
'a' | 'b' | 'c'
result parse overa
thenb
thenc
in the conditional. It makes each one trigger the conditional then unions the results together. Pretty awesome huh? It's kinda like a for-each loop for type unions.For our example
K extends K
will be evaluated for'a' | 'b' | 'c'
three times. Then there will be N! tuples built per iteration (because we recurse with N-1). The way TypeScript works is that unions are flat, so all the unions of the inner recursions will be lifted to the final level.
OK, so let's break down the "loops" in the distribution, so we can see what's happening. Here is a small cheat sheet for the chart:
type P = Permutation;
type X = Exclude
// Remember Permutation<never> => [] so P<never> => []
The final result of Permutation<1 | 2 | 3>
will be the values in the "Result" column union-ed together. (unified?)
If you want to see the definition for Permutation
, click me
type Permutation<T, K=T> =
[T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never
Iteration | T |
K in K extends K |
X<T, K> |
[K, ...P<X<T, K>>] |
Result |
---|---|---|---|---|---|
1 | 1 | 2 | 3 |
1 |
2 | 3 |
[1, ...P<2 | 3>] |
|
1.1 | 2 | 3 |
2 |
3 |
[1, 2, ...P<3>] |
|
1.1.1 | 3 |
3 |
never |
[1, 2, 3, ...[]] |
[1, 2, 3] |
1.2 | 2 | 3 |
3 |
2 |
[1, 3, ...P<2>] |
|
1.2.1 | 2 |
2 |
never |
[1, 3, 2, ...[]] |
[1, 3, 2] |
2 | 1 | 2 | 3 |
2 |
1 | 3 |
[2, ...P<1 | 3>] |
|
2.1 | 1 | 3 |
1 |
3 |
[2, 1, ...P<3>] |
|
2.1.1 | 3 |
3 |
never |
[2, 1, 3, ...[]] |
[2, 1, 3] |
2.2 | 1 | 3 |
3 |
1 |
[2, 3, ...P<1>] |
|
2.2.1 | 1 |
1 |
never |
[2, 3, 1, ...[]] |
[2, 3, 1] |
3 | 1 | 2 | 3 |
3 |
1 | 2 |
[3, ...P<1 | 2>] |
|
3.1 | 1 | 2 |
1 |
2 |
[3, 1, ...P<2>] |
|
3.1.1 | 2 |
2 |
never |
[3, 1, 2, ...[]] |
[3, 1, 2] |
3.2 | 1 | 2 |
2 |
1 |
[3, 2, ...P<1>] |
|
3.2.1 | 1 |
1 |
never |
[3, 2, 1, ...[]] |
[3, 2, 1] |
As mentioned earlier, TypeScript lifts all of the inner recursive unions and flattens them. More easily understood, the final type of Permutation<1 | 2 | 3>
will be the union of the "result" types in the right-hand column. So we will find Permutation<1 | 2 | 3>
is equivalent to:
[1,2,3] | [1,3,2] | [2,1,3] | [2,3,1] | [3,1,2] | [3,2,1]
And that concludes my long-winded explanation. I hope you enjoyed it!
Activity
sorokin-evgeni commentedon Jan 6, 2021
Great explanation, thank you.
But I have an error when try to run this code:
Type 'Permutation' is not generic.
It doesn't allow circular dependencies.eXamadeus commentedon Jan 6, 2021
@sorokin-evgeni What version of TypeScript are you running this with? I think 4.1 is the one that supports recursive conditional types.
This playground link should show that it works.
mistlog commentedon Mar 4, 2021
summary:
how to loop union:
how to check "T is never"
the answer:
ginobilee commentedon Mar 16, 2021
Great explanation. Now I know the
K
in[K, ...Permutation<Exclude<T, K>>]
is the distributed one, but at the first how do you know that theoretically?Dsan10s commentedon Mar 17, 2021
This is one of the best explanations I've seen on the subtleties of TypeScript, period. Thank you @eXamadeus !
eXamadeus commentedon Mar 21, 2021
@Dsan10s:
Thanks! I am really glad people are finding it useful.
@ginobilee:
Just making sure I understand the question, are you asking "how did I learn about the distribution of union types in a type conditional"?
ginobilee commentedon Mar 29, 2021
no, I mean there are tow 'K' in
[K, ...Permutation<Exclude<T, K>>]
, but they represent two different type variable; so it seems the firstK
means the distributed one, the secondK
is the original type variable, but how do you know it at the first, instinctively?cchudant commentedon Apr 10, 2021
That helped me a lot. Thank you!
49 remaining items