#9384β€’Hard

Maximum

Find the largest number in a tuple type with no greater-than operator in sight. A counting tuple eliminates union members until only the maximum remains.

TypeScript types have no greater-than operator, and this challenge asks you to find the largest number in a tuple anyway.

Maximum<T> takes an input tuple T and returns the maximum value in it. If T is an empty array, it returns never. Negative numbers are not considered. Since there is no > at the type level, you need to invent a way to compare numbers, and the answer here is a process of elimination driven by a counting tuple.

For example:

Maximum<[]> // never
Maximum<[0, 2, 1]> // 2
Maximum<[1, 20, 200, 150]> // 200

Once you've solved it, try the advanced follow-up: can you implement Minimum inspired by Maximum?

Challenge Instructions: Maximum

Hard

Description

Implement the type Maximum, which takes an input type T, and returns the maximum value in T.

If T is an empty array, it returns never. Negative numbers are not considered.

For example:

Maximum<[]> // never
Maximum<[0, 2, 1]> // 2
Maximum<[1, 20, 200, 150]> // 200

Advanced

Can you implement type Minimum inspired by Maximum?

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

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

Start with the full solution:

type Maximum<T extends any[], U = T[number], N extends unknown[] = []> = [
  U,
] extends [never]
  ? never
  : N['length'] extends U
    ? [Exclude<U, N['length']>] extends [never]
      ? N['length']
      : Maximum<T, Exclude<U, N['length']>, [...N, unknown]>
    : Maximum<T, U, [...N, unknown]>

The idea in one sentence: put all the numbers in a bag, count upward 0, 1, 2, 3, …, remove each number from the bag as you reach it, and the number whose removal empties the bag is the maximum. The rest is unpacking the machinery.

Defaulted type parameters as internal state

Maximum publicly takes one argument, but declares three. U = T[number] and N extends unknown[] = [] are defaulted parameters that callers never supply. They exist purely so the type can pass state to itself across recursive calls, the type-level equivalent of a helper function with extra accumulator arguments.

The empty-bag check, and why it's wrapped in brackets

[U] extends [never] is the termination test. Why not just U extends never? Because conditional types distribute over bare union type parameters: U extends never would split 0 | 2 | 1 into three separate checks and union the results, and for U = never (the empty union) there's nothing to distribute over, so the whole expression collapses to never instead of choosing a branch. Wrapping both sides in a one-element tuple, [U] extends [never], disables distribution and makes it an honest "is this exactly never?" question.

This same line also answers the Maximum<[]> case immediately: [][number] is never, so the empty tuple returns never before any counting starts.

Counting and eliminating

Each recursion step asks: is the current counter value in the bag?

Then the key question: did that removal empty the bag? [Exclude<U, N['length']>] extends [never] (bracketed again, for the same reason) checks it. If yes, every other number was removed at a smaller count, so the current N['length'] is the largest number in the tuple, and we return it. If no, larger numbers remain, so we recurse with the smaller bag and an incremented counter.

Tracing Maximum<[0, 2, 1]>:

// U = 0 | 2 | 1, count 0: 0 is in U, remove it β†’ 2 | 1 remains, keep going
// U = 2 | 1,     count 1: 1 is in U, remove it β†’ 2 remains, keep going
// U = 2,         count 2: 2 is in U, removing it empties the bag β†’ answer: 2

Duplicates cost nothing extra, by the way: a union automatically collapses [1, 1, 2] to 1 | 2.

Limits worth knowing

The counter takes one recursion step per integer from 0 up to the maximum, so Maximum<[1, 20, 200, 150]> runs 201 steps. That's fine, TypeScript allows roughly 1000 levels of this shape of recursion, but it's why the challenge excludes negative numbers (you can't count down to them from zero) and why huge values would blow the recursion budget. A comparison-based solution using digit-by-digit GreaterThan lifts those limits, at the cost of far more code.

Edge cases the tests cover

For the advanced follow-up, Minimum is a one-line twist: count upward the same way, and return the first counter value found in the union instead of the last one removed.

This challenge is originally from here.

Share this challenge

Learn the Concepts