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) | 0 | NaN | NaN |
' ' (whitespace) | 0 | NaN | NaN |
'123' | 123 | 123 | 123 |
'123.45' | 123.45 | 123 | 123.45 |
'123px' | NaN | 123 | 123 |
'0xFF' | 255 | 0 | 0 |
'1.5e3' | 1500 | 1 | 1500 |
' 42 ' | 42 | 42 | 42 |
'abc' | NaN | NaN | NaN |
null | 0 | NaN | NaN |
undefined | NaN | NaN | NaN |
Key takeaways from this table:
- Empty strings convert to
0with coercion methods butNaNwith 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):
- Unary plus (
+) - Fastest Number()- Nearly as fast as unary plusparseInt()- Moderate overheadparseFloat()- 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
| Scenario | Best Method | Why |
|---|---|---|
API response: { count: "42" } | + or Number() | Clean numeric strings |
| Form input validation | Number() | Strict validation needed |
CSS value: "16px" | parseInt(val, 10) | Extract integer, ignore unit |
Price: "$29.99" | parseFloat(cleaned) | Preserve decimals |
Query param: ?page=5 | Number() 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
+orNumber()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
0andNaNpropagating 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.