Skip to main content

How to Convert TypeScript Strings to Numbers

You're validating a form where users enter their age, but somehow "25px" slips through and crashes your calculation logic. Or maybe you're parsing query parameters from a URL, and parseInt keeps giving you unexpected results because you forgot the radix. String-to-number conversion seems simple until edge cases bite you.

This guide covers practical techniques for safely converting strings to numbers in TypeScript. You'll learn when to use each method, how to handle invalid inputs, and which conversion approach fits your specific scenario—whether you're processing API responses, validating form data, or parsing configuration values.

1. Understanding Coercion vs. Parsing

Before diving into specific methods, it's important to understand the difference between type coercion and parsing. It affects how each method handles edge cases.

Type coercion (used by Number() and unary +) attempts to convert the entire string value into a number. If any part of the string isn't numeric, you get NaN:

const price = Number('42.99'); // 42.99 ✓
const invalid = Number('42.99 USD'); // NaN ✗

Parsing (used by parseInt() and parseFloat()) reads the string from left to right and stops when it hits a non-numeric character, returning whatever it parsed up to that point:

const width = parseInt('720px', 10); // 720 (ignores 'px')
const height = parseFloat('1080.5vh'); // 1080.5 (ignores 'vh')

This distinction matters when you're extracting numeric values from formatted strings versus validating that an entire string is a valid number.

2. The Unary Plus Operator (+)

The fastest and most concise way to convert a string to a number is the unary plus operator. It performs the same coercion as Number() but with less typing:

// Converting API response data
const responseData = { count: '42', price: '29.99' };
const itemCount = +responseData.count; // 42
const itemPrice = +responseData.price; // 29.99

The unary plus operator is particularly useful when working with JSON data or API responses where numeric values arrive as strings. However, it requires the entire string to be numeric:

// Works with clean numeric strings
const validNumber = +'123.45'; // 123.45

// Returns NaN for mixed content
const invalidNumber = +'123px'; // NaN

// Also handles special cases
const fromNull = +null; // 0
const fromEmptyString = +''; // 0
const fromWhitespace = +' 42 '; // 42

When to use it: Quick conversions in expressions where you're confident the input is a clean numeric string. It's the go-to choice for experienced developers who prioritize conciseness and performance.

3. The Number Constructor

The Number() constructor provides explicit, readable type conversion. It's stricter than the unary operator in one way—it clearly communicates intent in your code:

// Parsing configuration values
const config = {
maxRetries: '3',
timeout: '5000',
enableCache: 'true'
};

const retries = Number(config.maxRetries); // 3
const timeoutMs = Number(config.timeout); // 5000

Number() is ideal when you need the entire string to represent a valid number. Unlike parsing methods, it won't silently ignore trailing characters:

// Strict validation - entire string must be numeric
const validPrice = Number('99.99'); // 99.99
const invalidPrice = Number('99.99 USD'); // NaN (good for validation)

// Handles different number formats
const scientific = Number('1.5e3'); // 1500
const negative = Number('-42.5'); // -42.5

When to use it: Validating user input or configuration values where you want to ensure the entire string is a valid number. The explicit function call makes your code more readable than the unary operator.

4. parseInt for Integer Values

When you need integer values or want to extract numbers from strings with trailing text, parseInt() is your tool. It reads characters from left to right until it hits something that isn't a digit:

// Extracting dimensions from CSS values
const cssWidth = '720px';
const cssHeight = '1080px';
const width = parseInt(cssWidth, 10); // 720
const height = parseInt(cssHeight, 10); // 1080

// Parsing user input
const ageInput = '25 years old';
const age = parseInt(ageInput, 10); // 25

Always specify the radix parameter (the second argument). Without it, you risk unexpected behavior, especially with strings that have leading zeros:

// Legacy JavaScript treated leading zeros as octal in older browsers
const withRadix = parseInt('08', 10); // 8 (decimal)
const withoutRadix = parseInt('08'); // 8 in modern browsers, but risky

// The radix also lets you parse different number systems
const binary = parseInt('1010', 2); // 10
const hex = parseInt('FF', 16); // 255
const octal = parseInt('77', 8); // 63

One common gotcha: parseInt() truncates decimal values rather than rounding them:

const truncated = parseInt('42.99', 10); // 42 (not 43)
const alsoTruncated = parseInt('99.1', 10); // 99

When to use it: Parsing integers from user input, extracting numeric values from formatted strings (like CSS units), or when you specifically need to discard decimal portions.

5. parseFloat for Decimal Precision

For decimal values, parseFloat() preserves the fractional part. Like parseInt(), it stops parsing when it encounters invalid characters:

// Parsing price strings
const priceString = '$29.99';
const price = parseFloat(priceString.substring(1)); // 29.99

// Extracting measurements
const temperature = parseFloat('98.6°F'); // 98.6 (ignores °F)
const distance = parseFloat('3.14159km'); // 3.14159

parseFloat() also handles scientific notation, which can be useful when working with API data:

const scientific = parseFloat('1.5e6'); // 1500000
const negative = parseFloat('-3.14e-2'); // -0.0314

One limitation: unlike parseInt(), parseFloat() doesn't accept a radix parameter—it only works with base-10 numbers:

// Handling formatted numbers with thousand separators
const formattedPrice = '1,234.56';
const cleaned = formattedPrice.replace(/,/g, '');
const actualPrice = parseFloat(cleaned); // 1234.56

When to use it: Processing currency values, scientific data, measurements, or any scenario where decimal precision matters.

6. Validating Conversion Results

String-to-number conversions don't throw errors—they return NaN for invalid inputs. You need to check the result yourself:

// Wrong: try-catch won't help here
try {
const number = Number('not-a-number');
console.log(number); // NaN (no error thrown)
} catch (error) {
// This never executes
}

// Right: check for NaN
const userInput = 'abc';
const converted = Number(userInput);

if (isNaN(converted)) {
console.log('Please enter a valid number');
} else {
console.log(`You entered: ${converted}`);
}

For production code, create validation utilities that return safer types:

// Returns null instead of NaN for easier null checks
function parseNumber(value: string): number | null {
const num = Number(value);
return isNaN(num) ? null : num;
}

// With range validation
function parsePositiveInt(value: string): number | null {
const num = parseInt(value, 10);
return !isNaN(num) && num > 0 ? num : null;
}

// For form validation
function parsePercentage(value: string): number | null {
const num = parseFloat(value);
return !isNaN(num) && num >= 0 && num <= 100 ? num : null;
}

// Usage
const age = parsePositiveInt('25'); // 25
const invalidAge = parsePositiveInt('-5'); // null
const discount = parsePercentage('15.5'); // 15.5
const invalidDiscount = parsePercentage('150'); // null

These utilities make validation explicit and prevent NaN from propagating through your application.

7. Edge Case Behavior Comparison

Different conversion methods handle edge cases differently. Here's a reference table showing how each method behaves with problematic inputs:

Input+str / Number()parseInt(str, 10)parseFloat(str)
'' (empty)0NaNNaN
' ' (whitespace)0NaNNaN
'123'123123123
'123.45'123.45123123.45
'123px'NaN123123
'0xFF'25500
'1.5e3'150011500
' 42 '424242
'abc'NaNNaNNaN
null0NaNNaN
undefinedNaNNaNNaN

Key takeaways from this table:

  • Empty strings convert to 0 with coercion methods but NaN with parsing methods
  • Parsing methods extract leading numbers from mixed content; coercion methods return NaN
  • Number() recognizes hex notation (0xFF); parseInt() needs explicit radix 16
  • Only coercion and parseFloat() properly handle scientific notation

8. Performance Considerations

If you're converting thousands of strings in a tight loop (like processing large datasets), performance differences matter. Here's how the methods compare:

Relative Performance (fastest to slowest):

  1. Unary plus (+) - Fastest
  2. Number() - Nearly as fast as unary plus
  3. parseInt() - Moderate overhead
  4. parseFloat() - Slightly slower than parseInt
// For bulk conversions where performance matters
const apiData = ['42', '1.5', '99', '3.14', '100'];

// Fastest approach
const numbers = apiData.map(str => +str);

// Nearly identical performance, more readable
const numbersExplicit = apiData.map(str => Number(str));

// Slower if you don't need parsing behavior
const integers = apiData.map(str => parseInt(str, 10));

In practice, the difference is negligible for typical use cases. Choose based on clarity and correctness first:

// Prioritize readability and correctness
function processQueryParam(value: string | null): number {
// Explicit validation is worth the tiny performance cost
if (!value) return 0;
const num = Number(value);
return isNaN(num) ? 0 : num;
}

Only optimize for speed when you've identified conversion as an actual bottleneck through profiling.

9. Common Pitfalls to Avoid

Forgetting the Radix in parseInt

This is the most common bug with parseInt(). In older JavaScript versions, strings starting with "0" were interpreted as octal:

// Dangerous in legacy environments
parseInt('08'); // Could be 0 in old browsers (invalid octal)
parseInt('010'); // Could be 8 in old browsers (octal interpretation)

// Safe in all environments
parseInt('08', 10); // Always 8
parseInt('010', 10); // Always 10

// Real-world example where this breaks
function parseZipCode(zip: string): number {
// Bug: ZIP codes like "08901" fail without radix
return parseInt(zip); // Wrong!

// Fix: always specify base 10
return parseInt(zip, 10); // Correct
}

Modern browsers default to base 10, but omitting the radix is still risky when you're maintaining code that might run in different environments.

Trusting Empty String Behavior

Empty strings convert to 0 with Number() and +, which can hide validation bugs:

// Dangerous: treats empty input as zero
function calculateTotal(priceInput: string): number {
return Number(priceInput) * 1.1; // Empty string becomes 0
}

calculateTotal(''); // Returns 0, no error

// Better: explicitly validate
function calculateTotalSafe(priceInput: string): number | null {
if (!priceInput.trim()) return null;
const price = Number(priceInput);
return isNaN(price) ? null : price * 1.1;
}

calculateTotalSafe(''); // Returns null (clearer intent)

Not Checking for NaN in Calculations

NaN propagates through math operations, silently breaking calculations:

// Bug: NaN propagates silently
function calculateDiscount(price: string, percent: string): number {
return Number(price) * (Number(percent) / 100);
}

calculateDiscount('99.99', 'abc'); // Returns NaN (no error)

// Fix: validate before calculating
function calculateDiscountSafe(
price: string,
percent: string
): number | null {
const priceNum = Number(price);
const percentNum = Number(percent);

if (isNaN(priceNum) || isNaN(percentNum)) {
return null;
}

return priceNum * (percentNum / 100);
}

calculateDiscountSafe('99.99', 'abc'); // Returns null

Confusing Parsing vs. Validation

parseInt() and parseFloat() extract numbers from mixed content, which isn't always what you want:

// Unexpected behavior with form input
function validateAge(input: string): boolean {
const age = parseInt(input, 10);
return !isNaN(age) && age >= 18;
}

validateAge('21'); // true ✓
validateAge('21 years'); // true (probably not what you want!)

// Better: validate entire string is numeric
function validateAgeStrict(input: string): boolean {
const age = Number(input);
return !isNaN(age) && age >= 18;
}

validateAgeStrict('21'); // true ✓
validateAgeStrict('21 years'); // false ✓

10. Choosing the Right Conversion Method

Here's a decision framework for selecting the appropriate method:

Use Unary Plus (+) When:

  • You need maximum performance (processing arrays, tight loops)
  • The code is internal and you control the input format
  • Conciseness matters and the context is clear
// Quick conversion in controlled environments
const ids = ['1', '2', '3'].map(id => +id);
const total = +apiResponse.total;

Use Number() When:

  • You need strict validation (entire string must be numeric)
  • Code readability is more important than brevity
  • You're validating user input or external data
// Form validation
function validateQuantity(input: string): number | null {
const num = Number(input);
return isNaN(num) ? null : num;
}

Use parseInt() When:

  • You need integers specifically
  • You want to extract numbers from formatted strings
  • You're working with different number systems (hex, octal, binary)
// Parsing CSS values
const fontSize = parseInt('16px', 10);

// Parsing hex color values
const red = parseInt('FF', 16);

Use parseFloat() When:

  • Decimal precision is important
  • You're extracting numbers from formatted measurement strings
  • You're working with scientific notation
// Processing measurements
const weight = parseFloat('72.5kg');

// Parsing scientific data
const distance = parseFloat('3.14e8');

Quick Reference Table

ScenarioBest MethodWhy
API response: { count: "42" }+ or Number()Clean numeric strings
Form input validationNumber()Strict validation needed
CSS value: "16px"parseInt(val, 10)Extract integer, ignore unit
Price: "$29.99"parseFloat(cleaned)Preserve decimals
Query param: ?page=5Number() or +Validate entire value
Scientific: "1.5e6"parseFloat()Handle notation
Hex color: "FF0000"parseInt(val, 16)Specific radix needed

11. Real-World Conversion Patterns

Parsing URL Query Parameters

Query strings always arrive as strings, even when they represent numbers:

// Extract page number from URL
function getPageFromQuery(searchParams: URLSearchParams): number {
const pageStr = searchParams.get('page');
if (!pageStr) return 1; // Default to first page

const page = Number(pageStr);
return isNaN(page) || page < 1 ? 1 : Math.floor(page);
}

// Usage
const url = new URL('https://example.com/products?page=3');
const currentPage = getPageFromQuery(url.searchParams); // 3

Processing Form Input

Forms always give you strings, even from number inputs:

function validatePriceInput(input: string): { valid: boolean; value?: number; error?: string } {
const trimmed = input.trim();

if (!trimmed) {
return { valid: false, error: 'Price is required' };
}

const price = Number(trimmed);

if (isNaN(price)) {
return { valid: false, error: 'Please enter a valid number' };
}

if (price <= 0) {
return { valid: false, error: 'Price must be greater than zero' };
}

// Round to 2 decimal places for currency
return { valid: true, value: Math.round(price * 100) / 100 };
}

// Usage
const result = validatePriceInput('29.99');
if (result.valid) {
console.log(`Price: $${result.value}`);
}

Handling API Responses

Sometimes APIs return numbers as strings (especially from JSON):

interface ApiProductResponse {
id: string;
name: string;
price: string; // API returns price as string
inventory: string;
}

function normalizeProduct(raw: ApiProductResponse) {
return {
id: raw.id,
name: raw.name,
price: +raw.price, // Fast conversion for controlled data
inventory: +raw.inventory
};
}

Extracting Numbers from Formatted Strings

When working with localized or formatted data:

// Parse currency with thousand separators
function parseCurrency(value: string): number | null {
// Remove currency symbols, spaces, and commas
const cleaned = value.replace(/[$€£,\s]/g, '');
const num = parseFloat(cleaned);
return isNaN(num) ? null : num;
}

parseCurrency('$1,234.56'); // 1234.56
parseCurrency('€ 1.234,56'); // 1234.56 (if using replace appropriately)

// Parse measurements
function parseMeasurement(value: string): { value: number; unit: string } | null {
const match = value.match(/^([\d.]+)\s*([a-z]+)$/i);
if (!match) return null;

const num = parseFloat(match[1]);
if (isNaN(num)) return null;

return { value: num, unit: match[2] };
}

parseMeasurement('42.5 kg'); // { value: 42.5, unit: 'kg' }
parseMeasurement('1080px'); // { value: 1080, unit: 'px' }

Key Takeaways

String-to-number conversion in TypeScript comes down to understanding the difference between coercion and parsing, then picking the right tool:

  • Use unary + or Number() when you need the entire string to be a valid number (coercion)
  • Use parseInt() when extracting integers from mixed strings or working with different number systems (always include the radix)
  • Use parseFloat() when decimal precision matters and you're parsing formatted strings
  • Always validate conversion results with isNaN() checks—these functions don't throw errors
  • Watch out for edge cases like empty strings converting to 0 and NaN propagating through calculations

The method you choose affects not just correctness but code clarity. When validating user input, explicit Number() calls make your intent clear. When parsing CSS values or API data with known formats, parsing functions get the job done. And when performance matters in tight loops, the unary operator gives you the speed you need.

Master these patterns and you'll handle numeric conversions confidently, whether you're building forms, processing API data, or working with complex TypeScript applications.