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.
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:
Type 'string' is not assignable to type 'number'.ts(2322)or at runtime:
console.log(Number("abc")) // NaNor in logs:
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:
// ❌ 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:
// ❌ 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:
// ❌ Silent failure: no error, but NaN propagates
const input = "abc";
const value = parseFloat(input); // NaN
console.log(value === 0); // false — NaN ≠ 0All 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:
- TypeScript compiles to JavaScript, and JavaScript has no
numbervsstringdistinction at runtime beyondtypeof. - Type assertions (
as number) are erased — they don’t change runtime behavior. - 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:
// 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:
// 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); // 42That 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#
- Create a
toNumberutility insrc/lib/convert.ts. - Replace all
as numberassertions withtoNumber(value). - Wrap API responses or form inputs with
toNumberbefore passing to numeric operations. - Add tests for
"0x1F","+3.14"," 1e3 ", and"abc".
Verify the fix#
Run this test in your REPL or Jest suite:
// 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:
// ❌ 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:
// ✅ 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:
- Always narrow types before conversion. Use
typeof value === 'string'guards before parsing. - Add runtime validation in API routes. In Next.js, validate
req.bodyfields before passing to business logic:
// 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:
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.
Related#
- Interfaces vs Types in TypeScript: 2026 Best Practices
- Complete Type Safety Guide for Next.js and Supabase with TypeScript
- TypeScript Migration Guide 2026: Upgrade JavaScript Projects Safely
- TypeScript Bang Operator ! Explained: Fix "Object
- Fix "Parsing error: Cannot find module next/babel"
- What is TypeScript and why should I use it instead of JavaSc
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Related Guides
TypeScript ! Operator Explained: Fix Object is possibly undefined
What the ! operator does in TypeScript, why it causes "Object is possibly undefined" errors, and how to replace it safely.
Interfaces vs Types in TypeScript: 2026 Best Practices
Clarify when to use interfaces vs types in TypeScript for scalable production apps — with concrete examples and real-world tradeoffs.
What is TypeScript and why should I use it instead of JavaSc
Complete guide to what is typescript and why should i use it instead of javasc. Covers root causes, diagnostic steps, and working code fixes.