Yuzhe's Blog

yuzhes

String to Number

String to Number

Challenge Link

Challenge

Convert a string literal type to a number literal type, behaving like Number.parseInt.

type Result = ToNumber<'123'>  // expected: 123

Solution

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

That’s it — one line. TypeScript 4.8 introduced infer with extends constraints in template literal types, which makes this almost trivial.

How it works

S extends `${infer N extends number}` ? N : never

This tells TypeScript: “try to interpret the string S as a template containing a number. If it can, infer that number as N.”

When S = '123', TypeScript recognizes that '123' matches the template `${number}` and infers N = 123 as a numeric literal type.

Walkthrough:

ToNumber<'123'>
→ '123' extends `${infer N extends number}` ? N : never
→ N = 123
→ 123 ✓

ToNumber<'0'>
→ N = 0 ✓

ToNumber<'abc'>
→ 'abc' does not match `${number}`
→ never ✓

Deep Dive

Pre-4.8 solution

Before TypeScript 4.8, infer ... extends didn’t exist. The classic approach uses a build-up helper:

type ToNumber<S extends string, A extends any[] = []> =
  `${A['length']}` extends S
    ? A['length']
    : ToNumber<S, [...A, 1]>

This counts up from 0 by growing a tuple A until its length (as a string) matches S. It works but hits recursion limits around ~999.

How the old approach works:

ToNumber<'3'>

A = []        → '0' extends '3'? No
A = [1]       → '1' extends '3'? No
A = [1,1]     → '2' extends '3'? No
A = [1,1,1]   → '3' extends '3'? Yes → A['length'] = 3 ✓

Why infer N extends number works

TypeScript 4.8 added the ability to constrain inferred types in conditional types. In template literal position:

`${infer N extends number}`

TypeScript’s template literal inference engine tries to parse the string as a number. If successful, N is narrowed to the specific numeric literal. This works for:

What doesn’t parse

ToNumber<''>        // never (empty string)
ToNumber<'abc'>     // never (not a number)
ToNumber<'12px'>    // never (unlike parseInt, no partial parsing)
ToNumber<' 42'>     // never (leading space)

Note that unlike JavaScript’s parseInt, the type-level version requires the entire string to be a valid number — no partial parsing.

Also works for other primitives

The same pattern works for booleans and bigints:

type ToBool<S extends string> =
  S extends `${infer B extends boolean}` ? B : never

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

type T1 = ToBool<'true'>   // true
type T2 = ToBool<'false'>  // false
type T3 = ToBI<'42'>       // 42n

Negative numbers and edge cases

type T1 = ToNumber<'-1'>     // -1 ✓
type T2 = ToNumber<'0'>      // 0 ✓
type T3 = ToNumber<'1e5'>    // never (scientific notation not supported)
type T4 = ToNumber<'Infinity'> // never

Key Takeaways