#7561Extreme

Subtract

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 never

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

Challenge Instructions: Subtract

Extreme

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 never

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

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ 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>>,
]

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

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'] : never

Two types, and the arithmetic never touches a number.

Numbers you can count on

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.

Building a tuple of a given length

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.

Peeling the subtrahend off

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'] = 1

The 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: never

A one-element tuple cannot start with two elements, the match fails, and never is returned without a single explicit comparison of M against S.

Why 1000 has to fail

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.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges