#14078•Medium

Modulo

Implement the % operator at the type level. Numbers become tuples, the divisor is peeled off the front repeatedly, and the leftover length is the remainder.

The type system has no % operator, and no arithmetic at all. You build it out of tuple lengths.

Modulo<M, N> returns the remainder of M divided by N, the same value JavaScript's % gives you for non-negative integers. Division itself never appears in the solution: you count instead. This counting-with-tuples pattern is the foundation of every type-level arithmetic challenge, so it pays off well beyond this one.

5 % 4 === 1     // Since 5 divided by 4 is 1 with a remainder of 1
9 % 4 === 1     // Since 9 divided by 4 is 2 with a remainder of 1
16 % 2 === 0    // Since 16 divided by 2 is 8 with a remainder of 0

You do not have to handle negative operands.

Challenge Instructions: Modulo

Medium

Javascript and a lot of other programming languages support the modulo (also know as remainder) operator % which returns the remainder when an integer is divided by another integer. For Javascript's implementation - see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder.

Here's some examples:

5 % 4 === 1     // Since 5 divided by 4 is 1 with a remainder of 1
9 % 4 === 1     // Since 9 divided by 4 is 2 with a remainder of 1
16 % 2 === 0    // Since 16 divided by 2 is 8 with a remainder of 0

You do not need to worry about the case when the left or right hand sides are negative.

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

type Modulo<M extends number, N extends number> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<Modulo<5, 4>, 1>>,
  Expect<Equal<Modulo<9, 4>, 1>>,
  Expect<Equal<Modulo<16, 2>, 0>>,
  Expect<Equal<Modulo<35, 1>, 0>>,
  Expect<Equal<Modulo<35, 10>, 5>>
]

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

Monthly subscription.
Cancel anytime + 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type Tuple<N extends number, Acc extends unknown[] = []> = Acc['length'] extends N
  ? Acc
  : Tuple<N, [...Acc, unknown]>
 
type Remainder<M extends unknown[], N extends unknown[]> = M extends [...N, ...infer Rest]
  ? Remainder<Rest, N>
  : M['length']
 
type Modulo<M extends number, N extends number> = Remainder<Tuple<M>, Tuple<N>>

Three small types, each doing one job.

Numbers as tuples

TypeScript cannot add or subtract numeric literals, but it can read ['length'] off a tuple and it can build tuples with the spread syntax. That gives you a bridge in both directions: 5 becomes a five-element tuple, and a five-element tuple becomes 5 again.

Tuple<N> crosses that bridge. The second parameter Acc is an accumulator: a working value that gets threaded through the recursion with a default, so the caller never has to pass it. Each step compares the length built so far against the target and either stops or appends one more unknown.

// Tuple<3> unfolds like this:
// Acc = []                            → 0 extends 3? no  → recurse
// Acc = [unknown]                     → 1 extends 3? no  → recurse
// Acc = [unknown, unknown]            → 2 extends 3? no  → recurse
// Acc = [unknown, unknown, unknown]   → 3 extends 3? yes → return Acc

The element type is irrelevant here. Only the length carries information, so unknown is as good a filler as any.

Peeling off the divisor

Remainder is where the actual work happens, and it rests on one pattern:

[object Object]

Read that as a question about shape: does M start with as many elements as N has? A tuple pattern may contain one rest element whose length is open. Since N is a concrete tuple by the time Remainder runs, its length is fixed, so there is exactly one way to split M, and Rest captures everything after that prefix. The infer keyword declares Rest on the spot and binds it to whatever matched.

That single check is subtraction in disguise. Matching means M is at least N long, and Rest is M minus N elements.

// Remainder<Tuple<9>, Tuple<4>>
// M has 9 elements → strip 4 → Rest has 5 → recurse
// M has 5 elements → strip 4 → Rest has 1 → recurse
// M has 1 element  → no match                → return 1

When the pattern stops matching, fewer than N elements are left, which is precisely the definition of a remainder. At that point M['length'] converts the tuple back into a numeric literal and the recursion unwinds.

Why it terminates

Every recursive call of Remainder is made with a strictly shorter tuple, because the match only succeeds when at least one full divisor can be removed. A shrinking tuple cannot shrink forever, so the chain always reaches the non-matching branch. Tuple terminates for the same reason read backwards: Acc grows by one element per step and Acc['length'] eventually hits N.

Both recursions are bounded by the operands, not by a fixed budget, so the usual instantiation-depth limit applies. Values in the low hundreds are comfortable; five-digit operands are not.

Edge cases the tests cover

Negative operands are out of scope by the task statement, and a divisor of 0 has no meaningful answer here, the same way % 0 gives you NaN in JavaScript.

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

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

Or start solving right away: explore all TypeScript challenges