Skip to content

296 - Permutation (with explanations) #614

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

Open
eXamadeus opened this issue Jan 5, 2021 · 37 comments
Open

296 - Permutation (with explanations) #614

eXamadeus opened this issue Jan 5, 2021 · 37 comments
Labels
296 answer Share answers/solutions to a question en in English recommended Answers / solutions that provided by the official team or the original author

Comments

@eXamadeus
Copy link
Contributor

eXamadeus commented Jan 5, 2021

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

@eXamadeus eXamadeus added answer Share answers/solutions to a question en in English labels Jan 5, 2021
@github-actions github-actions bot added the 296 label Jan 5, 2021
@sorokin-evgeni
Copy link

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

eXamadeus commented 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.

@antfu antfu added the recommended Answers / solutions that provided by the official team or the original author label Jan 7, 2021
@mistlog
Copy link
Contributor

mistlog commented Mar 4, 2021

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

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

Dsan10s commented Mar 17, 2021

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

@eXamadeus
Copy link
Contributor Author

eXamadeus commented Mar 21, 2021

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

ginobilee commented Mar 29, 2021

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

That helped me a lot. Thank you!

@patrick100
Copy link

I think there is a mistake in the table. In iteration 3.1.1, 'T' should be 2, and 'K in K extends K' also.

@eXamadeus
Copy link
Contributor Author

I think there is a mistake in the table. In iteration 3.1.1, 'T' should be 2, and 'K in K extends K' also.

Whoa, good catch! Thanks. I'm just glad someone read the whole thing, haha. I'll update it immediately.

@callqh
Copy link

callqh commented Sep 14, 2021

nb

@eXamadeus
Copy link
Contributor Author

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

Sorry I was slow to answer. Actually, both of the K's in [K, ...Permutation<Exclude<T, K>>] are the same K. Both are the distributed type. The Exclude<T, K> bit is removing the distributed type K from the union T.

This was referenced Jan 18, 2022
@Lionad-Morotar
Copy link
Contributor

awesome

@Finesse
Copy link

Finesse commented May 27, 2022

K extends K

The right part can be any type that K can be assigned to, TypeScript will iterate over every possible K subtype anyway. For example:

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

@yiheinchai
Copy link

Thank you for this explanation!
I was confused as to WHY you were checking [T] = [never] until I realised it was the base case for recursion.

@wangi4myself
Copy link

know a lot from this article,thanks!!
[T] extends [never] is the essential not only for Expect<Equal<Permutation, []>> but also the base case for recursion.

@idebbarh
Copy link

Thank youuu

@jaxkashif34
Copy link

@eXamadeus Thanks for this great explanation❤ got to learn about distributive conditionals. can you help me understand
At the end of iteration 1.1.1 Exclude return never and I belive that never will pass in Permutation in 1.2 iteration and value of T will be never and when [T] excludes [never] ? [] : ..... returns []. I know I'm misunderstanding something can you help me to understand how iteration 1.2 will start with 2 | 3 . thanks

@NameWjp
Copy link
Contributor

NameWjp commented May 30, 2023

awesome!

@Regestrac
Copy link

Great explanation...!

@rxMATTEO
Copy link

Lovely language. Enjoying every second, when i'm solving challenges in this repo.

@eXamadeus
Copy link
Contributor Author

@eXamadeus Thanks for this great explanation❤ got to learn about distributive conditionals. can you help me understand At the end of iteration 1.1.1 Exclude return never and I belive that never will pass in Permutation in 1.2 iteration and value of T will be never and when [T] excludes [never] ? [] : ..... returns []. I know I'm misunderstanding something can you help me to understand how iteration 1.2 will start with 2 | 3 . thanks

Glad you liked it!

It's a complicated chart, but essentially 1.1.1 is a "terminal" iteration. It would probably be better represented in a graph format, but a table was what I had. If you look at the value of T in 1.1 and 1.2, you will see that they're the same value. The K in K extends K value is the next iteration of distribution over the union K.

Iteration 1.2 is a follow up to 1.1 (where 1.1.1 is a termination of 1.1). Looking back, I wish I chose a better numbering system. It's kind of confusing.

@nomoreyou
Copy link

不会英语只能说声卧槽牛逼

@oliverfunk
Copy link

oliverfunk commented Apr 29, 2024

@eXamadeus how would one do something like the following in a single type?

type FullyPermuted =
  | Permutation<"a" | "b" | "c">
  | Permutation<"a" | "b">
  | Permutation<"b" | "c">

Where you would use it like:

type FullyPermuted = FullPermutation<"a" | "b" | "c">

Would also be interesting to see one with each distinct value too:

type PermutedDistinct =
  | Permutation<"a" | "b" | "c">
  | Permutation<"a" | "b">
  | Permutation<"b" | "c">
  | "a"
  | "b"
  | "c";

@Jayce-liang
Copy link

so nice!!!

@devshinthant
Copy link

@eXamadeus when you said "there is no such thing as a recursive union" , so isn't this a recursive union?
export interface CSSProperties extends React.CSSProperties {
[key: string]: any;
}

type MantineStyle = CSSProperties | ((theme: MantineTheme) => CSSProperties);
export type MantineStyleProp = MantineStyle | MantineStyle[] | MantineStyleProp[] | undefined;

@eXamadeus
Copy link
Contributor Author

eXamadeus commented Sep 5, 2024

@ShinnTNT What I meant to say is "there is no such thing as a union of unions". I'll update my post.

Recursive unions are a thing, but they have special rules. For example type A = A | B is illegal; however, type A = A[] | B is totally valid.

I was trying to talk about recursive distribution over a "union of unions" (aka nested unions), so thanks for pointing out that mistake! I updated the whole paragraph to the following:

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

Let me know if that is clearer.

@eXamadeus
Copy link
Contributor Author

eXamadeus commented Sep 5, 2024

@eXamadeus how would one do something like the following in a single type?

type FullyPermuted =
  | Permutation<"a" | "b" | "c">
  | Permutation<"a" | "b">
  | Permutation<"b" | "c">

Where you would use it like:

type FullyPermuted = FullPermutation<"a" | "b" | "c">

Would also be interesting to see one with each distinct value too:

type PermutedDistinct =
  | Permutation<"a" | "b" | "c">
  | Permutation<"a" | "b">
  | Permutation<"b" | "c">
  | "a"
  | "b"
  | "c";

For the first example, that seems somewhat complicated. It's a permutation of three items, then a union of all permutations of two of those three items (BTW, I think you missed Permutation<'a' | 'c'>). Hmm, I'm not sure at the moment. I know it's doable, but it might be quite ugly.

Personally, I think the way you wrote it makes the most sense. Not to say it's not possible, as it almost certainly is. But at what cost, haha?

Also, I think it would be easier to do the second one, since it's likely harder to have a "stop" value instead of drilling all the way down to single items.

@devshinthant
Copy link

@eXamadeus Excellent, that's a good point and more clearer.

@Keith-Web3
Copy link

Goated explanation 🔥

@shx123qwe
Copy link

Answer Playground Link (TypeScript 4.1+ only)

Answer Playground 链接(仅限 TypeScript 4.1+)

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 😆哇。。。那看起来很奇怪。别担心,我会把它全部分解;Weird Piece,由 Weird Piece 😆 提供

TLDR by @mistlog (Click me) TLDR 作者 (Click me)
Excellent Chinese translation by @zhaoyao91 (Click me)
优秀的中文翻译 (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.那在 16 位地狱的深处是什么?很高兴你问了。我的朋友们是 TypeScript 的一个 “特性”。这最终是有道理的,所以请耐心等待。

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?假设您想创建一个名为 . assertNever 此函数将用于检查以确保类型是...井。。。 never 。非常简单的概念。你可能想用它来检查你正在构建的一些奇怪的类型或其他东西。随它滚,好吗?

Wanna know a juicy secret? (Click me)
想知道一个多汁的秘密吗?(点选我)
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.很好,正是我们想要的。这应该会引发错误,因为 string 不能分配给 never .为什么 “ string not being assignable to never ” 意味着我们遇到了错误?因为 value param in assertNever 的预期类型将是 false when T extends never is false 和 string extends never is false。由于我们总是传递给 true 函数,因此我们得到了一个我们想要的错误。

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?“ boolean 不能分配给类型 never 为 ”?但。。。参数应该只对吗 true | false ?如果 T extends never 应该是 true ,如果 T extends never 不是,它应该是 false ,对吧?

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.嗯,事实证明 T extends neverT = never 但不是因为与条件有关时不起作用。TypeScript 在将泛型解包到条件语句中时做了一件有趣的事情:它分发它们。

Minor Primer on Distributive Conditional Types (Click me)
分布式条件类型的次要入门 (Click me)
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...因此,让我们将其重新绑定到 分发 中。 never TypeScript 对类型联合进行递归分发。还要注意的是,没有工会的联合这样的东西,工会的联合只是一个更大的工会,其中包含所有工会的所有元素......

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.无论如何,这的本质是:TypeScript 在分配条件时被视为 never 空联合。这意味着 'a' | never when getting distributed 被缩短为 'a' when disdistributioning。这也意味着 'a' | (never | 'b') | (never | never) 在分发时只是 become 'a' | 'b' ,因为该 never 部分等价于一个空的联合,我们可以合并所有的联合。

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?因此,将这一切引入其中,TypeScript 在通过条件分配时简单地忽略空联合。有道理吧?当没有可分发的内容时,为什么要在 Conditional 上进行分发呢?

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.既然我们知道了这一点,我们知道 T extends never 作为条件永远不会起作用(双关语)。那么我们如何告诉 TypeScript 不要被视为 never 空联合呢?好吧,我们可以强制 TypeScript 在尝试分发之前对其进行评估 T 。这意味着我们需要改变条件中的 T 类型,以便 of never T 的值被捕获并且不会丢失。我们这样做是因为我们不能在 . never 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):幸运的是,有几种简单的方法可以做到这一点!其中一种是直接 T 拍打到一个元组: [T] .这可能是最简单的。另一种方法是制作一个数组 TT[] 。这两个示例都有效,并且将 “求值” T 为非在它尝试在 conditional 上分布 never 之前的东西。以下是两种方法的工作示例( playground 链接):

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 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?**唉!我们现在用知识武装起来。想想吧。。。让我们看看你是否能猜出为什么会出现在这里......我给你一个提示:条件类型中的 union 会发生什么情况?

The answer... (Click me)
答案...(点选我)
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
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 the more you know

you are literally a genius!!!

@2083335157
Copy link

Oh no! I forgot TypeScript has the exclude generic tool.

type Permutation<T, U = T> = [T] extends [never]
  ? []
  : T extends T ?
    [T, ...[U] extends [T | infer Rest] ? Permutation<Rest extends T ? never : Rest> : never]
    : never

@stepantsuk
Copy link

Image

@IvanVelikanov
Copy link

IvanVelikanov commented Apr 12, 2025

oliverfunk

@eXamadeus how would one do something like the following in a single type?

type FullyPermuted =
| Permutation<"a" | "b" | "c">
| Permutation<"a" | "b">
| Permutation<"b" | "c">
Where you would use it like:

type FullyPermuted = FullPermutation<"a" | "b" | "c">
Would also be interesting to see one with each distinct value too:

type PermutedDistinct =
| Permutation<"a" | "b" | "c">
| Permutation<"a" | "b">
| Permutation<"b" | "c">
| "a"
| "b"
| "c";

Not sure if the question is still relevant but here's a possible solution:

type FullyPermuted<Type> = _FullyPermuted<Type>;

type _FullyPermuted<
  Type,
  Copy = Type,
  Result extends ReadonlyArray<unknown> = [],
> = [Type] extends [never]
  ? []
  : Type extends Copy
  ? _FullyPermuted<
      Copy,
      Copy extends Type ? never : Copy,
      [...Result, Type]
    >
  : Result;

type TestFullyPermuted = FullyPermuted<'A' | 'B' | 'C'>;

// Results in:
type _TestFullyPermuted =
  | ['A']
  | ['A', 'B']
  | ['A', 'B', 'C']
  | ['C']
  | ['A', 'C']
  | ['A', 'C', 'B']
  | ['B']
  | ['B', 'A']
  | ['B', 'A', 'C']
  | ['B', 'C']
  | ['B', 'C', 'A']
  | ['C', 'A']
  | ['C', 'A', 'B']
  | ['C', 'B']
  | ['C', 'B', 'A'];

It's pretty similar to @eXamadeus solution, but with 3 main differences:

  1. It uses tail-call optimization;
  2. Instead of the built-in Exclude, I used the "under-the-hood" implementation: Copy extends Type ? never : Copy. Doesn't make a difference, just a personal preference to avoid built-ins in these challenges.
  3. When recursing, it only excludes from the second generic (the Copy) while keeping the original intact. That's really the key to looping over every possibility of every union-length. It starts to make sense once you squint at it long enough :)
    If not, maybe I'll muster up the patience to write a similarly detailed explanation as @eXamadeus did for this challenge - we shall see :)

Hope it helps!

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
296 answer Share answers/solutions to a question en in English recommended Answers / solutions that provided by the official team or the original author
Projects
None yet
Development

No branches or pull requests