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.EqualEither 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.
Implement a type-level integers comparator. We've provided an enum for indicating the comparison result, like this:
a is greater than b, type should be Comparison.Greater.a and b are equal, type should be Comparison.Equal.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).
/* _____________ 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<EqUnlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
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.
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.EqualCompareDigitwise 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 answerBecause 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.
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.
Comparator<-5, 0> and Comparator<0, -5>: zero stringifies to '0' with no sign, so it takes the non-negative branch and the mixed-sign rules apply.Comparator<9007199254740991, 9007199254740992>: sixteen digits, equal width, decided by the last one. Nothing counts, so magnitude costs nothing.Comparator<-100, -1> is Lower: CompareLengths reports Greater for '100' against '1', and Flip turns it around.Comparator<31.415, 3.1415> is Greater: the integer parts differ in width, so the fractions are never inspected.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