Subtract one number literal from another by building tuples and peeling one off the other. TypeScript's 1000-step recursion budget is part of the puzzle.
The type system has no minus operator, so you count instead.
Subtract<M, S> computes M - S for two number literal types, where M is the minuend and S is the subtrahend. If the minuend is smaller than the subtrahend the result is never, because there are no negative results here.
Subtract<2, 1> // expect to be 1
Subtract<1, 2> // expect to be neverThe fourth test case is the interesting one. It is wrapped in // @ts-expect-error, which means Subtract<1000, 999> is required to fail compilation. The recursion limit you normally fight against is, for once, part of the specification.
Implement the type Subtraction that is - in Javascript by using BuildTuple.
If the minuend is less than the subtrahend, it should be never.
It's a simple version.
For example
Subtract<2, 1> // expect to be 1
Subtract<1, 2> // expect to be neverView on GitHub: https://tsch.js.org/7561
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
// M => minuend, S => subtrahend
type Subtract<M extends number, S extends number> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
Expect<Equal<Subtract<1, 1>, 0>>,
Expect<Equal<Subtract<2, 1>, 1>>,
Expect<Equal<Subtract<1, 2>, never>>,
// @ts-expect-error
Expect<Equal<Subtract<1000, 999>, 1>>,
]
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type BuildTuple<
N extends number,
Acc extends unknown[] = [],
> = Acc['length'] extends N ? Acc : BuildTuple<N, [...Acc, unknown]>
type Subtract<M extends number, S extends number> =
BuildTuple<M> extends [...BuildTuple<S>, ...infer Rest] ? Rest['length'] : neverTwo types, and the arithmetic never touches a number.
TypeScript cannot add or subtract number literals. What it can do is report the length of a tuple as a literal type: [unknown, unknown]['length'] is exactly 2, not number. That single fact is the foundation of every type-level arithmetic trick. The plan is to turn both numbers into tuples of that many elements, do the work by removing elements, then read the length back out.
BuildTuple<N> grows an accumulator until it is the requested size:
BuildTuple<3>
// → BuildTuple<3, [unknown]>
// → BuildTuple<3, [unknown, unknown]>
// → BuildTuple<3, [unknown, unknown, unknown]>
// → [unknown, unknown, unknown]The check Acc['length'] extends N is the exit condition. Note that the accumulator is passed as a type argument rather than wrapped around the recursive call. Writing it the other way, [unknown, ...BuildTuple<Minus1<N>>], would leave work to do after each recursive call returns, and the compiler would have to stack every level. When the recursive call is the whole branch, as it is here, TypeScript treats it as tail recursion and runs it as a loop.
BuildTuple<0> returns the default [] immediately, since []['length'] is already 0. The element type unknown is arbitrary; nothing ever reads these slots, only counts them.
With both numbers as tuples, subtraction becomes pattern matching:
[object Object]Read the pattern as a shape: a tuple that starts with S elements and then has some unknown tail. BuildTuple<S> is resolved to a concrete fixed-length tuple before the match runs, so the pattern is a fixed prefix followed by one variadic infer, which is exactly what a tuple pattern is allowed to contain. Whatever survives the prefix is captured in Rest, and its length is the answer.
For Subtract<2, 1>:
[unknown, unknown] extends [unknown, ...infer Rest]
// Rest = [unknown]
// Rest['length'] = 1The never case falls out for free. For Subtract<1, 2> the pattern needs at least two leading elements:
[unknown] extends [unknown, unknown, ...infer Rest]
// no match, so the conditional takes the false branch: neverA one-element tuple cannot start with two elements, the match fails, and never is returned without a single explicit comparison of M against S.
TypeScript evaluates tail-recursive conditional types as a loop, but that loop has a budget of 1000 instantiations. BuildTuple<N> uses N + 1 of them: one append per element, plus the final step that finds Acc['length'] matching N. So BuildTuple<999> fits with nothing to spare, and BuildTuple<1000> runs one step over and raises 'Type instantiation is excessively deep and possibly infinite'.
That error is what // @ts-expect-error on the last test case is waiting for. The challenge calls itself a simple version for this reason: the tuple approach is capped at 999, and going further means representing numbers digit by digit as string literals instead of counting elements one at a time.
Subtract<1, 1> → 0: the prefix consumes the whole tuple, Rest is [], and []['length'] is 0.Subtract<2, 1> → 1: one element survives the prefix.Subtract<1, 2> → never: the tuple is too short for the pattern, so the conditional falls through.Subtract<1000, 999> must error: a solution that somehow computed this would fail the test, because the unused @ts-expect-error directive would itself become an error.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges