The type-level version of Lodash intersection: compute what several lists have in common. Normalize every entry to a union and let & do the set math.
Lodash's intersection takes several arrays and returns the elements they all share. This challenge is the type version, with one twist: where Lodash only accepts arrays, the entries of your input tuple may each be an array, a bare type, or a union of types. Intersection<T> takes such a tuple T and returns a union of everything the entries have in common. Instead of comparing elements by hand, you normalize every entry into a union and let TypeScript's built-in intersection reduction do the set math for you.
type Res = Intersection<[[1, 2], [2, 3], [2, 2]]>; // expected to be 2
type Res1 = Intersection<[[1, 2, 3], [2, 3, 4], [2, 2, 3]]>; // expected to be 2 | 3
type Res2 = Intersection<[[1, 2], [3, 4], [5, 6]]>; // expected to be never
type Res3 = Intersection<[[1, 2, 3], [2, 3, 4], 3]>; // expected to be 3
type Res4 = Intersection<[[1, 2, 3], 2 | 3 | 4, 2 | 3]>; // expected to be 2 | 3
type Res5 = Intersection<[[1, 2, 3], 2, 3]>; // expected to be neverImplement the type version of Lodash.intersection with a little difference. Intersection<T> takes an Array T containing several arrays or any type element including the union type, and returns a new union containing all intersection elements.
type Res = Intersection<[[1, 2], [2, 3], [2, 2]]>; // expected to be 2
type Res1 = Intersection<[[1, 2, 3], [2, 3, 4], [2, 2, 3]]>; // expected to be 2 | 3
type Res2 = Intersection<[[1, 2], [3, 4], [5, 6]]>; // expected to be never
type Res3 = Intersection<[[1, 2, 3], [2, 3, 4], 3]>; // expected to be 3
type Res4 = Intersection<[[1, 2, 3], 2 | 3 | 4, 2 | 3]>; // expected to be 2 | 3
type Res5 = Intersection<[[1, 2, 3], 2, 3]>; // expected to be neverView on GitHub: https://tsch.js.org/5423
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The complete solution is short:
type Intersection<T> = T extends [infer First, ...infer Rest]
? (First extends unknown[] ? First[number] : First) & Intersection<Rest>
: unknownThree lines, one algorithm: convert every entry of the outer tuple into a union of its values, then intersect all those unions together.
T extends [infer First, ...infer Rest] is the standard head/tail destructuring pattern: First captures the first entry, Rest captures the remaining tuple. Each recursive step peels one entry off, so for [[1, 2], [2, 3], [2, 2]] the recursion visits [1, 2], then [2, 3], then [2, 2], and finally hits the empty tuple [], which doesn't match the pattern and falls into the base case.
The challenge allows three shapes of entry: an array, a plain type, or a union. They need a common currency before you can intersect them, and producing it is the inner conditional's job:
First extends unknown[] ? First[number] : First[1, 2, 3], then First[number] indexes it by number, producing the union of all its element types:[object Object]3 or a union like 2 | 3 | 4, it's already a union of candidates (a single type is a one-member union), so it passes through as-is.The T[number] indexed-access trick is worth memorizing: it's the type-level equivalent of "spread the array into a set of its values".
Each normalized entry is combined with & Intersection<Rest>. An element belongs to the intersection of several sets exactly when it belongs to all of them, which is what & on unions of literals computes. TypeScript reduces such intersections eagerly:
// (1 | 2) & (2 | 3) β 2
// (1 | 2) & (3 | 4) β neverDistribution does the work: the compiler cross-multiplies the union members, keeps the compatible combinations (2 & 2 is 2), and discards impossible ones (1 & 3 is never, which vanishes from the result). The type-level & is the set intersection; there is no manual "does this element appear in every list" loop. If you've done the Union to Intersection challenge, where 'foo' & 42 survives un-collapsed, note the difference: a directly written intersection is preserved as-is, while the reduction of 1 & 3 to never happens when the intersection shows up as a member of a union being normalized, which is the situation here.
unknown and not neverWhen the tuple is exhausted, the recursion returns unknown. That choice is load-bearing: unknown is the identity element for intersection (X & unknown is X), so it terminates the chain without disturbing the accumulated result. Returning never instead would collapse every result, since X & never is never. (Compare with building up a union recursively, where the roles flip and never is the harmless base case.)
For the first test the full expansion is:
[object Object]Res2: three arrays with nothing in common. Every cross-combination is incompatible, so the whole intersection reduces to never.Res3: mixes arrays with the bare literal 3. The literal skips the First[number] step and acts as a one-member set; (1 | 2 | 3) & (2 | 3 | 4) & 3 is 3.Res4: union entries like 2 | 3 | 4 also pass through unchanged, and the result 2 | 3 shows intersections can keep multiple members.Res5: two different bare literals, 2 and 3. No single type is both, so 2 & 3 gives never.When a problem says "find what several type-level collections have in common", don't iterate and compare. Normalize everything to unions and let & reduce. The compiler already implements the set logic; your job is to feed it the right shapes.
This challenge is originally from here.