Permutation Type In TypeScript

fix this type for me type Modifier = 'hover' | 'active' | 'before' | 'after'; type Permutation<S> = S extends never ? '' : ${S}-${Permutation<S>} type P = Permutation<Modifier>;

You're close! But the problem with your current Permutation type is that it recursively references itself without narrowing the set, which causes an infinite type expansion. TypeScript doesn't allow unrestricted recursive type definitions like this.

To generate all possible permutations of string unions like 'hover' | 'active' | 'before' | 'after', you need to:

  1. Use conditional types.

  2. Exclude the current item from the next recursive call to avoid infinite loops.

Here's the correct way to define it:

ts
type Modifier = 'hover' | 'active' | 'before' | 'after'; type Permutation<S, T = S> = [S] extends [never] ? '' : S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never; type P = Permutation<Modifier>;

✅ Explanation:

  • S extends T: Iterates over each item in the union.

  • Exclude<T, S>: Ensures we don't include the same element twice.

  • '' for the base case (never).

  • ${S} and ${S}-${...} allow both single and compound combinations.

📌 Result:

P will now include values like:

'hover' | 'active' | 'before' | 'after' | 'hover-active' | 'hover-before' | ... | 'hover-active-before' | ... | 'hover-active-before-after' | ...

Let me know if you want to limit the depth or add a prefix like @ or : for each!

Explain the logic behind S extends T

Great question! Let's break down the logic behind S extends T in this context. Understanding this will help you see how recursive permutation types work in TypeScript.


🔍 S extends T — What it really means here

In this line:

ts
S extends T

We're iterating over each element of the union T using S. It’s a trick to map each member of a union one by one.

But to really understand how this works, let’s break it down step by step.


🧠 Think of a union as a set

ts
type Modifier = 'hover' | 'active' | 'before' | 'after';

We want to go through each item ('hover', 'active', etc.) and for each one, create permutations using the remaining ones.


👇 So what's happening in this line?

ts
S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never;

Let’s say T = 'hover' | 'active' | 'before' at some point. Now we loop through S by extending T.

This expands to:

ts
'hover' extends 'hover' | 'active' | 'before' ? 'hover' | `hover-${Permutation<'active' | 'before'>}` : never; 'active' extends 'hover' | 'active' | 'before' ? 'active' | `active-${Permutation<'hover' | 'before'>}` : never; 'before' extends 'hover' | 'active' | 'before' ? 'before' | `before-${Permutation<'hover' | 'active'>}` : never;

That gives us all permutations starting with:

  • hover, hover-active, hover-active-before, ...

  • active, active-hover, active-hover-before, ...

  • and so on...

So we are saying:

"For each S in T, give me S plus all permutations of the rest of T excluding S."


✅ Why it works

Because in TypeScript, when you write:

ts
S extends T ? ... : ...

and S is itself a union (like 'hover' | 'active'), TypeScript distributes the conditional across each element of S. That’s what allows this type-level loop.


Bonus: Exclude<T, S>

That’s what removes the currently used item (S) so we don’t repeat values in the permutation.

Example:

ts
Exclude<'hover' | 'active', 'hover'> = 'active'

TL;DR

  • S extends T is looping through each item in the union T.

  • For each S, it creates:

    • ${S}: the standalone

    • ${S}-${Permutation<...>}: recursive combinations with the rest

  • Exclude<T, S> ensures no repeats.

  • The recursion continues until the union is empty (never), which is the base case.

Let me know if you want to visualize this with a small example set step-by-step!

Explain this and S is itself a union (like 'hover' | 'active'), TypeScript distributes the conditional across each element of S. That’s what allows this type-level loop.

Absolutely! This is a core concept in TypeScript’s type system and it’s a little mind-blowing at first — but super powerful once you get it.


🚀 What "distribution" means in conditional types

Let’s start with a simple conditional type:

ts
type A<T> = T extends string ? true : false;

If you do:

ts
type Test = A<'hello'>; // ✅ true

Makes sense, right?

But what if T is a union?

ts
type Test = A<'hello' | 123>;

Now here’s the twist:
TypeScript will distribute the conditional type across each member of the union, like this:

ts
A<'hello'> | A<123> => true | false

So:

ts
type Test = true | false; // ✅ That's the result!

This behavior is intentional and allows many advanced type tricks.


🧪 Why this matters for permutations

Let’s say you define:

ts
type Modifier = 'hover' | 'active'; type MyType<S> = S extends Modifier ? `:${S}` : never; type Result = MyType<'hover' | 'active'>;

Even though you passed a union ('hover' | 'active') to MyType, what TypeScript actually does is:

ts
MyType<'hover'> | MyType<'active'> => ':hover' | ':active'

That’s what we mean by:

"TypeScript distributes the conditional across the union."


🔁 Why this allows a "type-level loop"

When we do:

ts
S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never;

And S is a union ('hover' | 'active' | 'before'), TypeScript turns it into:

ts
// First S = 'hover' 'hover' extends T ? 'hover' | 'hover-' + Permutation<...> : never // // then S = 'active' 'active' extends T ? 'active' | 'active-' + Permutation<...> : never // // then S = 'before' ...

So each branch of the union gets processed individually and merged back into a final result.

This is not recursion in the traditional sense — it's distribution + recursive type call, and that’s what makes permutation types possible.


✅ TL;DR Summary

Concept Explanation
Distributive conditional T extends U ? A : B becomes `A1
Why it works It processes each part of the union separately and merges the results
Used for Generating permutations, filtering, mapping types, etc.
Magic behind permutations You give a union → distribute → recursively remove one at a time → build combinations

Want me to walk you through a real example line-by-line with 2 elements like 'a' | 'b'?

Then why not T extends S and why S extends T?

Excellent follow-up! Let's dive into why we write S extends T instead of T extends S in recursive type patterns like permutations.


🧠 First, let's revisit the pattern:

ts
type Permutation<S, T = S> = [S] extends [never] ? '' : S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never;

Here:

  • T is the full original union ('hover' | 'active' | ...).

  • S is the current item we’re iterating through.

The key idea is:

We want to go through each S in the union T, and for each one, build a permutation with the remaining items.

So the question is:


❓ Why S extends T, not T extends S?

Let’s break it down:

S extends T ✅ Means: take each S from union T and build something with it
T extends S 🚫 Would mean: does the full union T match each individual S? That’s not helpful or distributive

Example:

ts
type Modifier = 'hover' | 'active'; type Bad<T> = T extends 'hover' ? true : false; type BadTest = Bad<'hover' | 'active'>; // ✅ Distributes // But if you flip it: type Weird<T> = 'hover' extends T ? true : false; type WeirdTest = Weird<'hover' | 'active'>; // ✅ Still true, but no distribution

See the difference?

  • S extends T lets you loop through union members one by one ('hover', 'active', etc.) — this is distributive.

  • T extends S checks: “does 'hover' | 'active' extend 'hover'?” (No! So this fails.)


🔄 And in the context of permutations?

You're trying to do:

For each S in the union T,
add ${S} to ${Permutation<Exclude<T, S>>}

So the structure must be:

ts
S extends T

Which tells TypeScript:

“Hey, for each S inside T, go ahead and create a permutation branch with S.”

If you wrote T extends S, you’re essentially saying:

“For this one S, does the entire union T match or extend it?”
That’s a yes/no question, not a for-each loop.


✅ TL;DR

Why S extends T? Because we want to distribute over each S in union T.
Why not T extends S? That would not iterate. It would just test a full match.
What does this enable? Distributive conditional types → essential for permutations.

Would you like a visual step-by-step with 'a' | 'b' | 'c' to see exactly what happens in each recursive call?

Why [S] extends [never] ?

🔥 That’s one of the most clever tricks in TypeScript’s type system — and your curiosity is 100% on point!

Let’s break it down:

❓ Why do we write [S] extends [never] instead of just S extends never?


✅ First: What we’re doing

In this line:

ts
[S] extends [never] ? '' : ...

We’re checking if the union S is empty (i.e., never).
This tells us we’ve reached the end of recursion — a base case.


🧨 But here’s the twist:

If you write:

ts
S extends never ? ... : ...

It behaves differently than you'd expect when S is a union.

For example:

ts
type T = 'a' | 'b'; type X = T extends never ? true : false; // ❌ Result: false

You might think:

"If T is not never, then this should just be false."

True... but there’s a hidden issue when S is a union: TypeScript distributes conditional types over unions.

So:

ts
S extends never ? ... : ...

becomes:

ts
('a' extends never ? ... : ...) | ('b' extends never ? ... : ...)

Each branch is false, so the result is false.
But that's not the question we were trying to ask.

We wanted to ask:

“Is the whole union S equal to never?”
But S extends never ends up checking: “Are each of the members of S assignable to never?” – which they are not.


🧠 So what’s the solution?

We wrap it in a tuple:

ts
[S] extends [never]

Now it's not distributive anymore.
Why? Because TypeScript only distributes conditional types over naked type parameters — not when they’re wrapped.

So:

ts
[S] extends [never]

is evaluated once, as a whole.


📌 Real example:

ts
type T1 = 'a' | 'b'; type T2 = never; type A = T1 extends never ? true : false; // false (but distributed!) type B = [T1] extends [never] ? true : false; // false ✅ type C = [T2] extends [never] ? true : false; // true ✅

So now we can safely check:

"Is this union S completely empty?"
→ Meaning: Have we finished the recursive breakdown?


✅ TL;DR

Expression Meaning
S extends never ✅ Checks each member of S separately (distributes over union) — not what we want
[S] extends [never] ✅ Checks if the whole type S is literally never — this is what we need

Let me know if you want a playground link or a diagram to visualize it better!

But I got this error Type 'T & S' is not assignable to type 'string | number | bigint | boolean | null | undefined'.

Ah yes — that error typically pops up when you use template literal types (like `${S}-${...}`) and the type S isn't guaranteed to be a string-compatible primitive (string | number | boolean | null | undefined | bigint).


🧨 Error explained

You're probably using a line like this:

ts
`${S}-${Permutation<Exclude<T, S>>}`

But S is inferred from a union type like 'hover' | 'active' | 'before' | 'after'.
Inside that template string, TypeScript wants to make sure that S is something it can safely "interpolate" into a string.

And sometimes TypeScript gets confused and treats S as a generic type (maybe S & T) and can’t verify it's a valid type for a string template literal.


✅ Solution: constrain S and T to string

You can fix this by making sure S and T are explicitly constrained to string types.

ts
type Permutation<S extends string, T extends string = S> = [S] extends [never] ? '' : S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never;

This ensures both S and T are always string literals — and makes the template literals work safely.


🧪 Why this works

Without extends string, TypeScript might infer S as something weird like T & S (an intersection), which can’t go inside a string literal — hence the error you got:

Type 'T & S' is not assignable to type 'string | number | bigint | boolean | null | undefined'.

✅ Final working version

ts
type Modifier = 'hover' | 'active' | 'before' | 'after'; type Permutation<S extends string, T extends string = S> = [S] extends [never] ? '' : S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never; type P = Permutation<Modifier>;

Let me know if you also want to:

  • Limit depth of permutations (e.g., max 2 words)

  • Add prefixes like @ or : (e.g., :hover-before)

  • Generate these values into a mapped type

I can help you extend it in any direction.

why? Without extends string, TypeScript might infer S as something weird like T & S (an intersection), which can’t go inside a string literal — hence the error you got:

Awesome question — this gets right into the heart of how TypeScript handles generics in template literals when you don't constrain them.


💡 Quick answer:

If you don’t write S extends string, then TypeScript treats S and T as generic types — which might not be string-compatible.

And when used like this:

ts
`${S}-${Something}`

TypeScript says:

“Wait — I don’t know what S is. It could be anything — number? object? array? Maybe it's a weird intersection like T & S.”

And unless it's guaranteed to be a string-compatible primitive (string | number | boolean | bigint | null | undefined), it throws an error.


🧠 Deeper explanation: why T & S appears

Let’s imagine we define:

ts
type Permutation<S, T = S>

Now inside this recursive type, TypeScript sometimes needs to resolve both S and T in a template literal like:

ts
`${S}-${Permutation<Exclude<T, S>>}`

At this point:

  • TypeScript doesn't yet know that S is a string.

  • And T is inferred as the full original union.

  • So when it evaluates the substitution types, it could infer S and T as intersecting types (due to internal inference or type operations).

Thus, it might treat ${S} as ${T & S} in internal calculations — which may not be valid for template string use.

And that's why you get this error:

Type 'T & S' is not assignable to type 'string | number | bigint | boolean | null | undefined'.


✅ Solution: Constrain your generic

ts
type Permutation<S extends string, T extends string = S>

Now you're telling TypeScript:

“Hey — S and T are definitely strings (or string literals). You can safely use them inside template strings.”

So ${S} and ${S}-${...} are totally valid now, and the error goes away.


🧪 Real example

Try this in a playground:

ts
type Weird<S> = `${S}`; // ❌ Error type Works<S extends string> = `${S}`; // ✅ Fine

Without extends string, TypeScript will not assume it can safely coerce S into a string — even if you think it’s clear.


✅ TL;DR

Problem Why it happens
${S} errors Because S might not be a string — could be T & S, or something unknown
Fix Use S extends string to constrain it
Result Now TypeScript knows S is safe for template literals

Let me know if you want a visual diagram of how TypeScript expands and infers the types inside recursive templates — I can draw out the whole mental model for you.

how to do this? Limit depth of permutations (e.g., max 2 words)

Great question! 🧠
Limiting the depth of recursive permutations in TypeScript (e.g., to max 2 words like "hover-active") is totally doable — but it requires adding a "depth counter" to the recursion.


✅ Goal:

Instead of getting:

ts
"hover" "hover-active" "hover-active-before" "hover-active-before-after" // ❌ too deep

You want to stop after 2 elements:

ts
"hover" "hover-active" "active-before" ... // ✅ no 3-word combinations

🛠 Solution: Add a depth parameter

We can pass a tuple to count recursion depth — TypeScript tracks tuple length for free!

ts
type Modifier = 'hover' | 'active' | 'before' | 'after'; type Permutation< S extends string, T extends string = S, D extends any[] = [] // 👈 depth counter > = [S] extends [never] ? '' : D['length'] extends 2 // 👈 stop at depth 2 ? `${S}` : S extends T ? `${S}` | `${S}-${Permutation<Exclude<T, S>, Exclude<T, S>, [...D, 1]>}` : never;

🔍 Explanation:

  • D is a tuple that grows with each recursive step ([...D, 1])

  • D['length'] tracks how many steps deep we are

  • We stop recursion if depth = 2, and return only ${S}

  • Otherwise, we keep building ${S}-${...}


🔁 Example Output (type P):

ts
type P = Permutation<Modifier>;

Will include:

"hover" "active" "before" "after" "hover-active" "hover-before" "hover-after" "active-hover" "active-before" ...

❌ But not things like "hover-active-before" or "before-hover-active-after"


✅ TL;DR

To limit permutation depth:

  • Use a D extends any[] parameter as a counter

  • Use D['length'] extends 2 (or any number) as your depth limit

  • Push to it via [...D, 1] on recursion


Want to also allow only specific depths? Like exactly 2 words? Or at least 2? I can help with that too.

How to about a mapped type?

Ahhh 🔥 now we’re talking about real power — generating permutations as a mapped type to create an object-like structure from the permutations. Let's break it down:


✅ Goal: From Permutations → Mapped Type

Imagine you have this:

ts
type Modifier = 'hover' | 'active' | 'before' | 'after'; type P = Permutation<Modifier>; // produces: 'hover', 'hover-active', etc.

Now you want a mapped type like this:

ts
type ModifierMap = { 'hover': true; 'hover-active': true; 'before-after': true; ... }

🔧 Step-by-step

We’ll use three pieces:

1. Permutations with limited depth

Same as before, using a D extends any[] tuple for depth tracking:

ts
type Permutation< S extends string, T extends string = S, D extends any[] = [] > = [S] extends [never] ? never : D['length'] extends 2 ? S : S extends T ? S | `${S}-${Permutation<Exclude<T, S>, Exclude<T, S>, [...D, 1]>}` : never;

This returns a union of strings like:

ts
"hover" | "hover-active" | "before-after" ...

2. Create a mapped type from that union

Now you just map over it:

ts
type Modifier = 'hover' | 'active' | 'before' | 'after'; type LimitedPermutation = Permutation<Modifier>; type ModifierMap = { [K in LimitedPermutation]: true; };

This gives you a type like:

ts
type ModifierMap = { 'hover': true; 'hover-active': true; 'hover-before': true; // ... }

✅ Want values to be string or something else?

You can customize it:

ts
type ModifierMap = { [K in LimitedPermutation]: `:${K}`; // prefix with colon };

Or:

ts
type ModifierMap = { [K in LimitedPermutation]: K[]; // array of that key };

🧠 Summary

Task Code
Generate permutations Recursive Permutation<> type
Limit depth Use D extends any[] as depth tracker
Convert union to object Use mapped type: [K in Perms]: Value

Let me know if you’d like:

  • To generate only 2-word permutations (not 1, not 3)

  • To output the mapped type as a nested object like { hover: { active: true } }

  • To create keys with prefixes (:hover, @before, etc.)

I’d be happy to help with that too!

based on the conversation we had add comments to this code for me so that I don't miss out a point I might need to understand this code. Make sure to pay enough attention to the quetions I asked you.

Absolutely, here’s your code with detailed comments, carefully tailored to everything you’ve asked so far — including distribution, S extends T, template literals, depth limiting, and mapped types.


ts
// The base union of modifier types type Modifier = 'hover' | 'active' | 'before' | 'after'; /** * Recursively builds all unique permutations of the given union `S`, * up to a maximum depth of 2. * * @template S - The current item being permuted (starts as the full union). * @template T - The full union of items to permute (default: same as S). * @template D - A depth tracker implemented via tuple length (default: empty). * * Important concepts: * - S extends T is used to iterate through each member of the union. * - [S] extends [never] prevents distribution, used to detect the base case. * - D['length'] is used to limit how deep recursion can go (here: 2 levels). * - Template literal types allow us to create combinations like `hover-active`. */ type Permutation< S extends string, // Current item, constrained to string to avoid template literal errors T extends string = S, // Original full union (remains unchanged through recursion) D extends any[] = [] // Depth counter as tuple; grows on each recursion > = // Base case: if S is empty (`never`), stop recursion and return never [S] extends [never] ? never // If we've hit the depth limit (2), only return the current item, don't recurse further : D['length'] extends 2 ? S // Main recursive case: // For each item `S` in `T` (thanks to distribution over union), // build a string of `S` and recurse on the remaining items (excluding current S) : S extends T ? S | `${S}-${Permutation<Exclude<T, S>, Exclude<T, S>, [...D, 1]>}` : never; // All permutations up to 2 words type LimitedPermutation = Permutation<Modifier>; /** * Converts the union of permutation strings into a mapped type. * * Each key is a permutation string (e.g., "hover-active"), * and the value is simply `true`. You could change it to anything: string, function, etc. */ type ModifierMap = { [K in LimitedPermutation]: true; };

🧠 Recap of key learning points (inline with your questions):

Concept In code
S extends T Used to distribute over each member of the union
[S] extends [never] Prevents distribution and checks if union is empty
✅ Depth limiting Done using D extends any[], checked via D['length'] extends 2
✅ Template literal safety S and T are constrained to string to avoid errors like T & S
✅ Mapped type [K in LimitedPermutation] turns union into an object-like structure

Let me know if you'd like to also generate this as a runtime object, or type-check it against CSS class variants, or auto-document it.

No I meant this code type Modifier = 'hover' | 'active' | 'before' | 'after'; type Permutation<S extends string, T extends string = S> = [S] extends [never] ? '' : S extends T ? ${S} | ${S}-${Permutation<Exclude<T, S>>} : never; type ModifierGroup = Permutation<Modifier>;

Ah got it! Let’s add thorough, well-structured comments to this specific version of the code, addressing all the concepts we’ve talked about — like S extends T, template literal safety, distributive types, why we use [S] extends [never], and what the type is doing at each step.

Here’s your annotated version:

ts
// Base union type of CSS-like state modifiers type Modifier = 'hover' | 'active' | 'before' | 'after'; /** * Generates all possible permutations of a union of strings (e.g., 'hover' | 'active' | ...) * in a recursive way — allowing combinations like: * 'hover' * 'hover-active' * 'hover-active-before' * ... * * This version does **not** limit the depth — it generates full-length permutations. * * @template S - The current value being distributed over the union. * @template T - The full union of strings. Default is same as S to preserve original union during recursion. */ type Permutation<S extends string, T extends string = S> = // Base case: when S becomes never (empty union), return the empty string. // We wrap S in a tuple to avoid TypeScript's distributive conditional behavior. [S] extends [never] ? '' // Main recursive logic: // "For each S in T" — this happens because conditional types distribute over unions. : S extends T // For each string S in the union T: // - include the single string `${S}` (base case) // - and also build `${S}-${...}` by recursively permuting the rest (excluding current S) ? `${S}` | `${S}-${Permutation<Exclude<T, S>>}` : never; /** * Final result: * This resolves to all string permutations of the Modifier union, * such as: * - 'hover' * - 'hover-active' * - 'hover-before' * - 'hover-active-before' * - ... */ type ModifierGroup = Permutation<Modifier>;

🧠 Key Points Recap from Our Conversation:

Concept How it applies in the code
S extends T Distributes S over T, enabling iteration over union members
[S] extends [never] Prevents distribution and checks if the union is empty (base case)
S extends string Ensures safety for template literals like `${S}-${...}`
❌ No depth limit This version creates permutations of any length until all elements are used
Exclude<T, S> Removes current S from union T to avoid repeated elements in permutations
✅ Recursive template literals Build new string combinations at each step of the recursion

Let me know if you'd like:

  • A version with depth limit added to this one

  • Runtime generation of the same keys

  • A way to visualize this type output in an IDE or playground

I can tailor it however you'd like.