#300β€’Hard

String to Number

Convert a string literal type to its number, like Number.parseInt but stricter. One infer extends clause replaces pages of digit-by-digit recursion.

Turn the string type '42' into the number type 42, with no runtime code involved.

ToNumber<S> converts a string literal to a number. It's inspired by Number.parseInt, though stricter, as you'll see below. Parsing strings into numbers at the type level is a building block for many advanced type utilities, from validating route parameters like '/users/42' to type-level math, and this challenge introduces the right tool for the job: infer ... extends.

For example

type T0 = ToNumber<'0'> // expected to be 0
type T1 = ToNumber<'27'> // expected to be 27
type T2 = ToNumber<'18@7_$%'> // expected to be never

Challenge Instructions: String to Number

Hard

Convert a string literal to a number, which behaves like Number.parseInt.

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

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

πŸ‘‹ Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The whole solution:

type ToNumber<S extends string> = S extends `${infer N extends number}`
  ? N
  : never

Three lines, but they rely on a feature that's easy to miss if you learned conditional types before TypeScript 4.8.

The hard way (and why we don't need it)

Historically, this challenge required building numbers digit by digit: split the string into characters, map each character to a digit, and multiply/accumulate using tuple lengths as numbers. Dozens of lines of recursive machinery.

TypeScript 4.8 made all of that obsolete by allowing a constraint directly on an infer declaration inside a template literal: `${infer N extends number}`. This tells the compiler two things at once:

When both hold, N is inferred as the actual number type, not the string. The compiler effectively runs the string-to-number conversion for you.

Walking through the matches

A few intermediate examples show what the pattern evaluates to:

// S = '27'      β†’ matches, N = 27      β†’ result: 27
// S = '0'       β†’ matches, N = 0       β†’ result: 0
// S = '18@7_$%' β†’ no valid number match β†’ result: never

For '27', the compiler asks: is there a number N such that `${N}` produces '27'? Yes, 27 itself, so N is inferred as the literal type 27.

For '18@7_$%', no number stringifies to exactly that text. Note the difference from Number.parseInt at runtime: parseInt('18@7_$%') would happily return 18 by reading the leading digits and stopping. The type-level pattern has no notion of "parse as much as you can". The entire string must round-trip through a number. Since it can't, the conditional falls into the false branch and we return never, which is what the test suite expects.

Why never and not an error?

Returning never for unparseable input is idiomatic for type-level functions: it composes well. A caller can test ToNumber<X> extends never ? ... : ... to branch on "was this a valid number string?", the same way runtime code checks for NaN.

Edge cases the tests cover

One caveat worth knowing for real-world use: the round-trip rule means strings with leading zeros like '007' also resolve to never (no number prints as '007'), and very large values may normalize to scientific notation. For this test suite, though, the three-line solution is complete. A single modern language feature collapses a page of recursive types into one conditional.

This challenge is originally from here.

Share this challenge

Learn the Concepts