How to Convert a String to Number in TypeScript: 2026 Fix
Developer Guide

How to Convert a String to Number in TypeScript: 2026 Fix

Fix silent NaN errors when converting strings to numbers in TypeScript. Learn explicit parsing, type narrowing, and runtime validation.

2026-06-12
8 min read
How to Convert a String to Number in TypeScript: 2026 Fix

TL;DR#

If you’re seeing NaN or runtime type errors when converting a string to a number in TypeScript, the issue is usually implicit coercion or missing runtime validation. Fix it by using explicit parsing (Number(), parseInt(), unary +) and narrowing the type before use.

If that doesn’t work, scroll to Verify the fix — there are two common variants this guide also covers.

What you'll see#

You’ll see one of these in your editor or terminal:

plaintext
Type 'string' is not assignable to type 'number'.ts(2322)

or at runtime:

plaintext
console.log(Number("abc")) // NaN

or in logs:

plaintext
Expected number, got string: "42"

It happens when you assign a string value (e.g., from an API response, form input, or environment variable) to a variable typed as number, or when you pass a string to a function expecting a number parameter — especially in Next.js API routes, Supabase edge functions, or n8n workflows where data is untyped by default.

The behavior is the same across Node.js, browsers, and Deno — because the issue isn’t environment-specific; it’s in how TypeScript handles type narrowing and coercion.

Common failure modes#

The most frequent failure is assuming TypeScript will infer or cast a string to a number automatically. For example:

ts
// ❌ Common mistake: assuming implicit conversion
const input = "42";
const value: number = input; // Type error: 'string' is not assignable to 'number'

Another is relying on unsafe type assertions:

ts
// ❌ Unsafe: bypasses type checking but fails at runtime
const input = "42";
const value = input as number; // Compiles, but typeof value === 'string'
console.log(value + 1); // "421" — string concatenation!

A third is using parseFloat without validation:

ts
// ❌ Silent failure: no error, but NaN propagates
const input = "abc";
const value = parseFloat(input); // NaN
console.log(value === 0); // false — NaN ≠ 0

All three stem from the same root: TypeScript’s type annotations are compile-time only. At runtime, "42" is still a string — and JavaScript’s coercion rules apply.

Diagnosis#

TypeScript’s typeof operator returns 'string' for any value that is a JavaScript string — even if you declared it as number. This is because:

  1. TypeScript compiles to JavaScript, and JavaScript has no number vs string distinction at runtime beyond typeof.
  2. Type assertions (as number) are erased — they don’t change runtime behavior.
  3. Functions like parseInt, Number, and unary + are the only ways to convert strings to numbers — and they require explicit invocation.

The relevant code path is:

ts
// src/lib/parseNumber.ts
export function parseInput(input: string): number {
  // ❌ This looks safe but isn’t — input is still a string
  const num = input as number;
  return num + 10; // Runtime: "42" + 10 = "421"
}
 
// ✅ Correct: explicit parsing + validation
export function parseInputSafe(input: string): number {
  const num = Number(input);
  if (Number.isNaN(num)) {
    throw new Error(`Invalid number string: "${input}"`);
  }
  return num;
}

The root cause is that TypeScript’s static type system cannot guarantee runtime type safety — it only prevents compile-time mismatches. If you accept untyped data (e.g., from req.body, process.env, or fetch), you must validate it before use.

The fix#

Use explicit parsing with runtime validation. Here’s the pattern I use in production:

ts
// src/lib/convert.ts
export function toNumber(value: unknown): number {
  if (typeof value === 'number') {
    return value;
  }
 
  if (typeof value === 'string') {
    const trimmed = value.trim();
    if (trimmed === '') {
      throw new Error('Empty string is not a valid number');
    }
    const num = Number(trimmed);
    if (Number.isNaN(num)) {
      throw new Error(`Cannot convert "${value}" to number`);
    }
    return num;
  }
 
  throw new Error(`Cannot convert ${typeof value} to number`);
}
 
// Usage
const input = "  42  ";
const result = toNumber(input); // 42

That single change addresses the cause because it enforces type safety at runtime — not just at compile time — and handles edge cases like whitespace, empty strings, and invalid formats.

Step by step#

  1. Create a toNumber utility in src/lib/convert.ts.
  2. Replace all as number assertions with toNumber(value).
  3. Wrap API responses or form inputs with toNumber before passing to numeric operations.
  4. Add tests for "0x1F", "+3.14", " 1e3 ", and "abc".

Verify the fix#

Run this test in your REPL or Jest suite:

ts
// src/lib/convert.test.ts
import { describe, it, expect } from 'vitest';
import { toNumber } from './convert';
 
describe('toNumber', () => {
  it('converts valid numeric strings', () => {
    expect(toNumber('42')).toBe(42);
    expect(toNumber('  42  ')).toBe(42);
    expect(toNumber('+3.14')).toBe(3.14);
    expect(toNumber('1e3')).toBe(1000);
    expect(toNumber('0xFF')).toBe(255);
  });
 
  it('throws on invalid input', () => {
    expect(() => toNumber('abc')).toThrow('Cannot convert "abc" to number');
    expect(() => toNumber('')).toThrow('Empty string is not a valid number');
    expect(() => toNumber(null)).toThrow('Cannot convert object to number');
  });
 
  it('preserves number inputs', () => {
    expect(toNumber(42)).toBe(42);
    expect(toNumber(3.14)).toBe(3.14);
  });
});

You should see all tests pass, and no NaN or type errors in your editor.

If you’re still seeing the error, two common variants exist:

Variant A — typeof returns 'string' even after as number#

This happens when you use as number to silence TypeScript but the value remains a string at runtime. The fix is to replace as number with Number(value) and add a runtime check:

ts
// ❌ Before
const value = input as number;
if (typeof value === 'number') { ... } // Always true — but value is still string!
 
// ✅ After
const num = Number(input);
if (typeof num === 'number' && !Number.isNaN(num)) {
  // Safe to use
}

Variant B — Number() returns NaN for hex or scientific notation#

Number("0xFF") and Number("1e3") work — but parseInt("0xFF", 10) does not. If you need hex support, use parseInt(value, 16) with explicit base:

ts
// ✅ Hex support
function toNumberHex(value: string): number {
  if (value.startsWith('0x') || value.startsWith('0X')) {
    const num = parseInt(value.slice(2), 16);
    if (Number.isNaN(num)) {
      throw new Error(`Invalid hex: "${value}"`);
    }
    return num;
  }
  return toNumber(value);
}

Why this happens (and how to avoid it next time)#

TypeScript’s type system is designed for static analysis — it catches mismatches before runtime but doesn’t enforce type safety at runtime. This is intentional: JavaScript’s dynamic nature allows flexibility, but it means you must validate external data.

To prevent regressions, adopt two habits:

  1. Always narrow types before conversion. Use typeof value === 'string' guards before parsing.
  2. Add runtime validation in API routes. In Next.js, validate req.body fields before passing to business logic:
ts
// pages/api/submit.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { toNumber } from '@/lib/convert';
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    const count = toNumber(req.body.count); // Safe, throws on invalid input
    res.json({ count });
  } catch (err) {
    res.status(400).json({ error: 'Invalid count' });
  }
}

I cover this pattern in detail in Complete Type Safety Guide for Next.js and Supabase with TypeScript, where we enforce runtime validation at the edge with Supabase Functions.

FAQ#

Q: Does this work with const strings?#

A: Yes — but only if the string is known at compile time. For example:

ts
const x: string = "42";
const y: number = Number(x); // ✅ Works — but still requires Number()

TypeScript won’t infer x as "42" unless you use a literal type (const x = "42" as const), but even then, Number(x) is required — TypeScript won’t auto-convert.

Q: Why does Number(" 42 ") work but parseInt(" 42 ") sometimes doesn’t?#

A: Number() trims whitespace automatically; parseInt() does not — it stops parsing at the first non-digit unless the string starts with whitespace. Always use Number(value.trim()) for consistency.

Q: Can I use the unary + operator?#

A: Yes — +"42" is valid and fast, but it’s less explicit and harder to lint. I prefer Number() for readability and to avoid confusion with +"" (which returns 0).

Q: What about BigInt or BigInt64Array?#

A: For values > Number.MAX_SAFE_INTEGER, use BigInt(value) — but note BigInt("42") requires a string, and Number() will truncate. I cover BigInt patterns in TypeScript Migration Guide 2026.

Frequently Asked Questions

|

Have more questions? Contact us

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.