#274Extreme

Integers Comparator

Compare two numeric literal types and return Greater, Equal or Lower. With no comparison operator available, the work happens on the digits of the stringified numbers.

Comparator<A, B> takes two numeric literal types and reports how they relate, using the Comparison enum the challenge hands you with its Greater, Equal and Lower members.

type A = Comparator<5, 8> // expected to be Comparison.Lower
type B = Comparator<-25, -30> // expected to be Comparison.Greater
type C = Comparator<0, 0> // expected to be Comparison.Equal

Either argument can be positive, negative or zero, and the signs can differ. The tuple-counting trick that powers most type-level arithmetic is no help here: the tests include 9007199254740992, and no recursion will ever count that high. What does survive is decimal notation. Convert both numbers to strings and the comparison turns into a walk over digits.

Challenge Instructions: Integers Comparator

Extreme

Implement a type-level integers comparator. We've provided an enum for indicating the comparison result, like this:

  • If a is greater than b, type should be Comparison.Greater.
  • If a and b are equal, type should be Comparison.Equal.
  • If a is lower than b, type should be Comparison.Lower.

Note that a and b can be positive integers or negative integers or zero, even one is positive while another one is negative.*

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

enum Comparison {
  Greater,
  Equal,
  Lower,
}

type Comparator<A extends number, B extends number> = any

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

type cases = [
  Expect<Equal<Comparator<5, 5>, Comparison.Equal>>,
  Expect<Equal<Comparator<5, 6>, Comparison.Lower>>,
  Expect<Equal<Comparator<5, 8>, Comparison.Lower>>,
  Expect<Equal<Comparator<5, 0>, Comparison.Greater>>,
  Expect<Equal<Comparator<-5, 0>, Comparison.Lower>>,
  Expect<Equal<Comparator<0, 0>, Comparison.Equal>>,
  Expect<Eq

Pro Challenge

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

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full:

type Digits = '0123456789'
 
type CompareDigit<X extends string, Y extends string> = X extends Y
  ? Comparison.Equal
  : Digits extends `${string}${X}${string}${Y}${string}`
    ? Comparison.Lower
    : Comparison.Greater
 
type Then<First extends Comparison, Second extends Comparison> =
  First extends Comparison.Equal ? Second : First
 
type CompareLengths<A extends string, B extends string> =
  A extends `${string}${infer ARest}`
    ? B extends `${string}${infer BRest}`
      ? CompareLengths<ARest, BRest>
      : Comparison.Greater
    : B extends '' ? Comparison.Equal : Comparison.Lower
 
type CompareDigitwise<A extends string, B extends string> =
  A extends `${infer X}${infer ARest}`
    ? B extends `${infer Y}${infer BRest}`
      ? Then<CompareDigit<X, Y>, CompareDigitwise<ARest, BRest>>
      : Comparison.Greater
    : B extends '' ? Comparison.Equal : Comparison.Lower
 
type IntegerPart<S extends string> = S extends `${infer I}.${string}` ? I : S
 
type CompareMagnitude<A extends string, B extends string> = Then<
  CompareLengths<IntegerPart<A>, IntegerPart<B>>,
  CompareDigitwise<A, B>
>
 
type Flip<C extends Comparison> = C extends Comparison.Greater
  ? Comparison.Lower
  : C extends Comparison.Lower ? Comparison.Greater : Comparison.Equal
 
type Comparator<A extends number, B extends number> =
  `${A}` extends `-${infer AMagnitude}`
    ? `${B}` extends `-${infer BMagnitude}`
      ? Flip<CompareMagnitude<AMagnitude, BMagnitude>>
      : Comparison.Lower
    : `${B}` extends `-${string}`
      ? Comparison.Greater
      : CompareMagnitude<`${A}`, `${B}`>

Read it from the bottom. `${A}` converts a numeric literal type to its string form, where a minus sign appears as a literal - character, so `${-25}` is '-25' and the pattern `-${infer AMagnitude}` captures '25'. Mixed signs settle the question on the spot. Matching signs hand the two magnitudes to CompareMagnitude, and when both were negative Flip inverts the verdict, because -25 beating -30 is the same fact as 30 beating 25.

Comparing two digits

CompareDigit needs an ordering over '0' to '9' without arithmetic. The string '0123456789' already encodes one: a digit is smaller than another when it appears earlier. Asking whether that string matches `${string}${X}${string}${Y}${string}` asks whether X sits somewhere before Y.

type One = CompareDigit<'3', '7'> // 3 comes before 7 → Comparison.Lower
type Two = CompareDigit<'7', '3'> // no 3 after the 7 → Comparison.Greater
type Three = CompareDigit<'4', '4'> // the X extends Y branch → Comparison.Equal

Walking two digit strings

CompareDigitwise compares character by character. The pattern `${infer X}${infer ARest}` peels off a single character: when two infer placeholders sit side by side, the first matches exactly one character and the second takes the rest. Each step compares the leading digits, and Then<First, Second> is the hand-off, returning Second only when First came back Equal. A difference therefore ends the recursion immediately.

// CompareDigitwise<'141', '142'>
//   '1' vs '1' → Equal, continue with '41' vs '42'
//   '4' vs '4' → Equal, continue with '1' vs '2'
//   '1' vs '2' → Lower, and that is the answer

Because a difference short-circuits, the two branches where one string runs out are only ever reached after every previous character matched. The longer string then wins, which is what makes the decimal tests work with no zero padding: '3.1415' against '3.141' agrees for five characters, then the leftover 5 decides.

Length before digits

Digitwise comparison assumes the digits line up, otherwise '9' would beat '100'. Stringified numbers never carry leading zeros, so a longer integer part always means a bigger number, and CompareLengths runs first. It is the same lockstep walk with the digit comparison removed: consume one character from each side and see which side empties first. Once the integer parts are known to be the same width, the decimal points line up too, so CompareDigitwise can run over the whole string and treat . as just another character that matches itself.

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