Skip to content

296 - Permutation (with explanations) #614

Open
@eXamadeus

Description

@eXamadeus
Contributor

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)

#614 (comment)

Excellent Chinese translation by @zhaoyao91 (Click me)

#614 (comment)

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

wat.

"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 where T = '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...

alright, I'm gonna go cry

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 over a then b then c 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!

the more you know

Activity

added
answerShare answers/solutions to a question
enin English
on Jan 5, 2021
sorokin-evgeni

sorokin-evgeni commented on Jan 6, 2021

@sorokin-evgeni

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

eXamadeus commented on Jan 6, 2021

@eXamadeus
ContributorAuthor

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

added
recommendedAnswers / solutions that provided by the official team or the original author
on Jan 7, 2021
mistlog

mistlog commented on Mar 4, 2021

@mistlog
Contributor

summary:

how to loop union:

type loopUnion<Union extends string, Item extends string = Union> = Item extends Item ? `loop ${Item}` : never;
type result = loopUnion<"A" | "B" | "C">; // "loop A" | "loop B" | "loop C" 

how to check "T is never"

type IsNever<T> = [T] extends [never] ? true : false;

the answer:

type Permutation<Union, Item = Union> = Item extends Item ? PermuteItem<Union, Item> : never;
type PermuteItem<Union, Item, Rest = Exclude<Union, Item>> = IsNever<Rest> extends true ? [Item] : [Item, ...Permutation<Rest>];
ginobilee

ginobilee commented on Mar 16, 2021

@ginobilee
K extends K
        ? [K, ...Permutation<Exclude<T, K>>]
        : never

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

Dsan10s commented on Mar 17, 2021

@Dsan10s

This is one of the best explanations I've seen on the subtleties of TypeScript, period. Thank you @eXamadeus !

eXamadeus

eXamadeus commented on Mar 21, 2021

@eXamadeus
ContributorAuthor

@Dsan10s:

This is one of the best explanations I've seen on the subtleties of TypeScript, period. Thank you @eXamadeus !

Thanks! I am really glad people are finding it useful.

@ginobilee:

K extends K
        ? [K, ...Permutation<Exclude<T, K>>]
        : never

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?

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

ginobilee commented on Mar 29, 2021

@ginobilee

@Dsan10s:

This is one of the best explanations I've seen on the subtleties of TypeScript, period. Thank you @eXamadeus !

Thanks! I am really glad people are finding it useful.

@ginobilee:

K extends K
        ? [K, ...Permutation<Exclude<T, K>>]
        : never

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?

Just making sure I understand the question, are you asking "how did I learn about the distribution of union types in a type conditional"?

no, I mean there are tow 'K' in [K, ...Permutation<Exclude<T, K>>], but they represent two different type variable; so it seems the first K means the distributed one, the second K is the original type variable, but how do you know it at the first, instinctively?

cchudant

cchudant commented on Apr 10, 2021

@cchudant

That helped me a lot. Thank you!

49 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    296answerShare answers/solutions to a questionenin EnglishrecommendedAnswers / solutions that provided by the official team or the original author

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @oliverfunk@Dsan10s@zhaoyao91@sorokin-evgeni@Finesse

        Issue actions

          296 - Permutation (with explanations) · Issue #614 · type-challenges/type-challenges