#5423β€’Hard

Intersection

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 never

Challenge Instructions: Intersection

Hard

Implement 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 never

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

Loading...

Detailed Explanation

The complete solution is short:

type Intersection<T> = T extends [infer First, ...infer Rest]
  ? (First extends unknown[] ? First[number] : First) & Intersection<Rest>
  : unknown

Three lines, one algorithm: convert every entry of the outer tuple into a union of its values, then intersect all those unions together.

Walking the outer tuple

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.

Normalizing each entry to a union

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:

[object Object]

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

Intersecting the unions

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)  β†’  never

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

The base case: why unknown and not never

When 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]

Edge cases the tests cover

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.

Share this challenge

Learn the Concepts