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]> // 200Once you've solved it, try the advanced follow-up: can you implement Minimum inspired by Maximum?
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]> // 200Can 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.
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.
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.
U starts as T[number], the union of all element types. For [0, 2, 1] that's 0 | 2 | 1. This is the "bag" of numbers still in play.N is the counter: a tuple whose length is the number currently being checked. Tuples are the standard way to count in the type system, because [...N, unknown] increments and N['length'] reads the value.[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.
Each recursion step asks: is the current counter value in the bag?
N['length'] extends U: for a counter of length 2, this asks whether 2 is a member of the union U. If it isn't, we increment. Maximum<T, U, [...N, unknown]> keeps the bag unchanged and moves on to the next number.Exclude<U, N['length']>. Exclude filters a union: Exclude<0 | 2 | 1, 1> is 0 | 2.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: 2Duplicates cost nothing extra, by the way: a union automatically collapses [1, 1, 2] to 1 | 2.
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.
Maximum<[]> β never: caught by the [U] extends [never] guard on the very first call.Maximum<[0, 2, 1]> β 2: verifies that 0, a value that's falsy and easy to mishandle, is eliminated like any other member.Maximum<[1, 20, 200, 150]> β 200: unsorted input with the maximum in the middle of the tuple. Order never mattered; the union threw away positions on day one.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.