#399β€’Hard

Tuple Filter

FilterOut<T, F> removes tuple elements assignable to F. The catch is never, which makes naked conditional checks silently evaporate.

A tuple filter sounds easy until the test suite hands you never.

FilterOut<T, F> removes every element assignable to F from the tuple T. The recursion itself is friendly, but the tests are built around the type that breaks naive solutions: never. To pass, you need to understand why conditional types silently disappear when handed never, and how wrapping both sides in a tuple switches that behavior off.

For example,

[object Object]

Challenge Instructions: Tuple Filter

Hard

Implement a type FilterOut<T, F> that filters out items of the given type F from the tuple T.

For example,

[object Object]

View on GitHub: https://tsch.js.org/399

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 solution first:

type FilterOut<T extends any[], F> = T extends [infer First, ...infer Rest]
  ? [First] extends [F]
    ? FilterOut<Rest, F>
    : [First, ...FilterOut<Rest, F>]
  : []

Five lines, one of which contains the entire difficulty of the challenge.

The recursion skeleton

T extends [infer First, ...infer Rest] splits the tuple into its head and everything after it, the type-level equivalent of const [first, ...rest] = arr:

// T = [1, never, 'a']
// First = 1
// Rest  = [never, 'a']

Each step makes one keep-or-drop decision about First and recurses on Rest:

When the tuple is empty, [] doesn't match the head/tail pattern and the : [] base case ends the recursion. That also directly answers the first test: FilterOut<[], never> is [].

Why not First extends F?

This is where naive solutions fail. The obvious membership check would be:

[object Object]

But conditional types have a special rule: when the type left of extends is a naked type parameter and you instantiate it with never, the conditional doesn't evaluate either branch. It short-circuits to never itself. (This is a side effect of distributivity: never is the empty union, so there's nothing to distribute over, and the result is empty.)

You can watch the damage in miniature:

type IsNever1<X> = X extends never ? true : false
type A = IsNever1<never>   // never  (not true)
 
type IsNever2<X> = [X] extends [never] ? true : false
type B = IsNever2<never>   // true

With the naked check, the moment First is never the whole keep-or-drop decision evaporates into never, and your output tuple silently corrupts. Since four of the six tests filter on never, this is what the challenge is probing.

The tuple-wrapping fix

[First] extends [F] wraps both sides in a one-element tuple. Now the thing left of extends is [First], a tuple type rather than a naked type parameter, so distributivity is disabled and the comparison happens normally. [never] extends [never] is an honest structural check that evaluates to true, so never elements are correctly dropped:

// FilterOut<['a', never], never>
// step 1: ['a'] extends [never]?  no  β†’ ['a', ...FilterOut<[never], never>]
// step 2: [never] extends [never]? yes β†’ FilterOut<[], never>
// step 3: base case β†’ []
// result: ['a']

The subtle final test

The last case deserves a close look:

FilterOut<[number | null | undefined, never], never | null | undefined>
// expected: [number | null | undefined]

The first element is a union that partially overlaps F. It should not be removed, and the tuple wrapping gives you that for free, in a second way. Because [First] extends [F] is non-distributive, the union is checked as a whole: number | null | undefined is only assignable to never | null | undefined if the number part fits too. It doesn't, so the whole element is kept intact. A distributive check would have split the union and tried to filter it piece by piece, which is not what a tuple filter should do. Elements are atomic; either the entire element matches F or it stays.

Edge cases the tests cover

The takeaway pattern: any time a type-level check must treat never (or unions) as a single value rather than distributing, wrap both sides in [...]. It's one of the most frequently needed tricks in the hard tier.

This challenge is originally from here.

Share this challenge

Learn the Concepts