Introduction to Type Conversion
JavaScript performs two kinds of type conversions:
- Explicit Type Conversion (Type Casting): When you manually convert one data type to another using functions like
String(),Number(), orBoolean(). - Implicit Type Conversion (Type Coercion): When JavaScript automatically converts data types during operations, often in ways that might surprise you!
Understanding type conversion is crucial for writing reliable JavaScript code and avoiding unexpected behaviors.
Analogy: Type Conversion as Language Translation
Think of type conversion like translating between different languages:
- Explicit conversion is like using a professional translator (you specifically ask for the translation and have control over it)
- Implicit conversion/coercion is like being in a foreign country where people automatically switch to English when they detect you're struggling with the local language (the system decides when and how to translate)
The problem with automatic translation (coercion) is that sometimes meanings get lost or distorted in the process, just like how JavaScript's automatic conversions can sometimes lead to unexpected results!
Explicit Type Conversion
Explicit type conversion, also called type casting, occurs when you deliberately convert a value from one type to another using JavaScript's built-in conversion functions.
Converting to Strings
// String() constructor
console.log(String(42)); // "42"
console.log(String(true)); // "true"
console.log(String(null)); // "null"
console.log(String(undefined)); // "undefined"
console.log(String([1, 2, 3])); // "1,2,3"
console.log(String({a: 1})); // "[object Object]"
// toString() method
console.log((42).toString()); // "42"
console.log(true.toString()); // "true"
// console.log(null.toString()); // Error: Cannot read property 'toString' of null
// console.log(undefined.toString()); // Error
console.log([1, 2, 3].toString()); // "1,2,3"
// Special cases with numbers
console.log((16).toString(16)); // "10" (hexadecimal)
console.log((16).toString(2)); // "10000" (binary)
console.log((16).toString(8)); // "20" (octal)
// Template literals
console.log(`Value: ${42}`); // "Value: 42"
console.log(`${true}`); // "true"
Converting to Numbers
// Number() constructor
console.log(Number("42")); // 42
console.log(Number("42.5")); // 42.5
console.log(Number("")); // 0
console.log(Number(" \n")); // 0 (whitespace becomes 0)
console.log(Number("42px")); // NaN (can't convert non-numeric strings)
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number([1])); // 1 (single element array)
console.log(Number([1, 2])); // NaN (multi-element array)
// parseInt/parseFloat functions
console.log(parseInt("42")); // 42
console.log(parseInt("42.9")); // 42 (ignores decimal part)
console.log(parseInt("42px")); // 42 (stops at non-numeric)
console.log(parseInt(" 42")); // 42 (ignores leading whitespace)
console.log(parseInt("a42")); // NaN (needs to start with numeric)
console.log(parseFloat("42.5")); // 42.5
console.log(parseFloat("42")); // 42
console.log(parseFloat("42.5px")); // 42.5 (stops at non-numeric)
// Specifying base with parseInt
console.log(parseInt("1010", 2)); // 10 (binary)
console.log(parseInt("ff", 16)); // 255 (hexadecimal)
console.log(parseInt("0xff")); // 255 (auto-detects hex with 0x prefix)
// Unary plus operator
console.log(+"42"); // 42
console.log(+"42.5"); // 42.5
console.log(+""); // 0
console.log(+true); // 1
console.log(+false); // 0
console.log(+"42px"); // NaN
Converting to Booleans
// Boolean() constructor
console.log(Boolean(42)); // true (non-zero numbers are truthy)
console.log(Boolean(0)); // false
console.log(Boolean(-0)); // false
console.log(Boolean("hello")); // true (non-empty strings are truthy)
console.log(Boolean("")); // false (empty string)
console.log(Boolean(" ")); // true (whitespace string is non-empty)
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean({})); // true (objects are truthy)
console.log(Boolean([])); // true (arrays are truthy)
// Double NOT (!!) operator
console.log(!!"hello"); // true
console.log(!!0); // false
console.log(!!undefined); // false
console.log(!![]); // true
Other Type Conversions
// Converting to BigInt
console.log(BigInt(42)); // 42n
console.log(BigInt("42")); // 42n
console.log(BigInt(true)); // 1n
// console.log(BigInt(42.5)); // RangeError: Cannot convert 42.5 to a BigInt
// console.log(BigInt("42.5")); // SyntaxError: Cannot convert 42.5 to a BigInt
// Object conversions
console.log(Object(42)); // Number {42}
console.log(Object("hello")); // String {"hello"}
console.log(Object(true)); // Boolean {true}
// Array from string
console.log(Array.from("hello")); // ["h", "e", "l", "l", "o"]
Real-World Example: Form Data Processing
When handling form data, you often need to convert between different types:
// Process a user registration form
function processRegistrationForm(formData) {
// Ensure clean data with appropriate types
const userData = {
// Trim and ensure string
username: String(formData.username).trim(),
// Convert to number and ensure valid
age: parseInt(formData.age, 10),
// Ensure boolean (checkbox values)
subscribedToNewsletter: Boolean(formData.newsletter),
// Array conversion (multi-select)
interests: formData.interests
? (Array.isArray(formData.interests)
? formData.interests
: [formData.interests])
: [],
// Default values for missing fields
membershipLevel: formData.membershipLevel || 'basic',
// Date conversion
registeredOn: new Date().toISOString()
};
// Validation
const errors = {};
if (userData.username.length < 3) {
errors.username = "Username must be at least 3 characters";
}
if (isNaN(userData.age) || userData.age < 18) {
errors.age = "Age must be a number and at least 18";
}
// Return processed data or errors
return {
isValid: Object.keys(errors).length === 0,
userData: userData,
errors: errors
};
}
// Test with sample form data
const formData = {
username: " johndoe ",
age: "25",
newsletter: "yes",
interests: ["programming", "music"]
};
const result = processRegistrationForm(formData);
console.log(result);
Implicit Type Conversion (Coercion)
Implicit type conversion, or coercion, happens automatically when you perform operations on values of different types. JavaScript tries to make sense of the operation by converting one or both values.
String Conversion Rules
// The + operator with strings
console.log("5" + 3); // "53" (number converted to string)
console.log(3 + "5"); // "35" (number converted to string)
console.log("5" + true); // "5true" (boolean converted to string)
console.log("5" + null); // "5null" (null converted to string)
console.log("5" + undefined); // "5undefined" (undefined converted to string)
// Arrays and + operator
console.log([1, 2] + [3, 4]); // "1,23,4" (arrays converted to strings)
console.log([1, 2] + 3); // "1,23" (both convert to strings)
// Objects and + operator
console.log({} + []); // "[object Object]" (object converted to string)
console.log({} + {}); // "[object Object][object Object]"
Numeric Conversion Rules
// Mathematical operators (except +)
console.log("5" - 3); // 2 (string converted to number)
console.log(3 - "5"); // -2 (string converted to number)
console.log("5" * 3); // 15 (string converted to number)
console.log("6" / 2); // 3 (string converted to number)
console.log("5" % 2); // 1 (string converted to number)
// Unary operators
console.log(+"5"); // 5 (string converted to number)
console.log(-"5"); // -5 (string converted to number)
// Comparison operators
console.log("5" > 3); // true (string converted to number)
console.log("5" < 10); // true (string converted to number)
console.log("5" >= "10"); // false (string comparison, "5" > "1")
console.log("5" >= 10); // false (string converted to number)
// Equality with coercion (==)
console.log("5" == 5); // true (string converted to number)
console.log(true == 1); // true (boolean converted to number)
console.log(false == 0); // true (boolean converted to number)
console.log(null == undefined); // true (special case)
console.log("0" == false); // true (both converted to numbers)
// No coercion with strict equality (===)
console.log("5" === 5); // false (different types)
console.log(true === 1); // false (different types)
console.log(null === undefined); // false (different types)
Boolean Conversion Rules
// Logical operators convert to boolean but return original values
// Logical AND (&&) returns first falsy or last truthy
console.log(0 && "hello"); // 0 (first operand is falsy)
console.log("hello" && 42); // 42 (last operand)
console.log("hello" && 0); // 0 (first falsy operand)
// Logical OR (||) returns first truthy or last falsy
console.log(0 || "hello"); // "hello" (first truthy operand)
console.log("hello" || 42); // "hello" (first operand is truthy)
console.log(0 || ""); // "" (last operand)
// Nullish coalescing (??) returns right side only for null or undefined
console.log(0 ?? "default"); // 0 (0 is not null or undefined)
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
console.log("" ?? "default"); // "" (empty string is not null or undefined)
// Logical NOT (!) converts to boolean and negates
console.log(!0); // true (0 is falsy)
console.log(!"hello"); // false (non-empty string is truthy)
console.log(!null); // true (null is falsy)
// Conditional (ternary) operator
console.log("hello" ? "truthy" : "falsy"); // "truthy"
console.log(0 ? "truthy" : "falsy"); // "falsy"
// Conditional statements
if ("hello") {
console.log("Strings are truthy");
}
if (!null) {
console.log("null is falsy");
}
Analogy: Coercion as Automatic Adapters
Think of JavaScript's type coercion like universal power adapters that airports have. When you plug your device (one type) into a foreign outlet (another type), the adapter automatically converts the connection to make it work.
This is convenient, but also potentially dangerous – just as you wouldn't want an adapter to supply the wrong voltage to your expensive electronics, JavaScript's automatic conversions can sometimes lead to unexpected results if you don't understand the rules.
Real-World Example: Shopping Cart Total
Type coercion bugs can easily creep into real applications:
// A buggy shopping cart calculation
function calculateCartTotal(cart) {
let total = 0;
for (let i = 0; i < cart.items.length; i++) {
const item = cart.items[i];
const itemPrice = item.price;
const quantity = item.quantity;
// BUG: This could be a string from a form!
total = total + itemPrice * quantity;
}
return "$" + total; // String coercion at the end
}
// Example with bug
const buggyCart = {
items: [
{ name: "Shirt", price: "25", quantity: 2 }, // Price as string
{ name: "Pants", price: 35, quantity: 1 },
{ name: "Hat", price: 15, quantity: 1 }
]
};
console.log(calculateCartTotal(buggyCart)); // "$252535" - Oops!
// Fixed version with explicit conversion
function calculateCartTotalFixed(cart) {
let total = 0;
for (let i = 0; i < cart.items.length; i++) {
const item = cart.items[i];
// Explicitly convert to numbers
const itemPrice = Number(item.price);
const quantity = Number(item.quantity);
if (isNaN(itemPrice) || isNaN(quantity)) {
console.warn(`Invalid price or quantity for ${item.name}`);
continue;
}
total += itemPrice * quantity;
}
// Format as currency with 2 decimal places
return "$" + total.toFixed(2);
}
console.log(calculateCartTotalFixed(buggyCart)); // "$100.00" - Correct!
The Equality Operators
One of the most confusing aspects of JavaScript coercion is how the equality operators work. JavaScript has two types of equality operators:
Loose Equality (==)
The loose equality operator == performs type coercion if the operands are of different types.
// Number-String comparisons
console.log(5 == "5"); // true (string converted to number)
console.log("5" == 5); // true (string converted to number)
// Boolean comparisons
console.log(1 == true); // true (true becomes 1)
console.log(0 == false); // true (false becomes 0)
console.log("1" == true); // true (both convert to number 1)
console.log("0" == false); // true (both convert to number 0)
// null and undefined
console.log(null == undefined); // true (special case)
console.log(null == 0); // false (null only equals undefined and itself)
console.log(undefined == 0); // false (undefined only equals null and itself)
// Objects and primitives
console.log([1, 2] == "1,2"); // true (array converts to string)
console.log("[object Object]" == {}); // true (object converts to string)
Strict Equality (===)
The strict equality operator === checks equality without type conversion.
// No coercion happens with ===
console.log(5 === "5"); // false (different types)
console.log(1 === true); // false (different types)
console.log(null === undefined); // false (different types)
console.log(0 === false); // false (different types)
console.log([1, 2] === "1,2"); // false (different types)
// Same type and value
console.log(5 === 5); // true
console.log("hello" === "hello"); // true
console.log(null === null); // true
Loose Equality Coercion Rules
The rules of == can be summarized as:
- If the types are the same, compare as strict equality.
- If comparing
nullandundefined, returntrue. - If comparing a number and a string, convert the string to a number.
- If one operand is a boolean, convert it to a number.
- If comparing an object with a primitive, convert the object to a primitive.
Real-World Example: Common Equality Bugs
Using loose equality incorrectly is a common source of bugs:
// Authentication bug
function checkUserAccess(userRole) {
// Bug: Using == instead of ===
if (userRole == "admin") {
grantAdminAccess();
}
}
// This works as expected
checkUserAccess("admin"); // Grants access
// But this also grants access unexpectedly!
checkUserAccess(["admin"]); // Array converts to string "admin"
// Fixed version
function checkUserAccessFixed(userRole) {
// Use strict equality
if (userRole === "admin") {
grantAdminAccess();
}
}
Coercion in Conditional Statements
JavaScript automatically converts values to booleans in contexts that require a boolean, such as conditional statements.
Conditions in if Statements
// Values converted to boolean in conditions
if ("hello") {
console.log("Non-empty strings are truthy");
}
if (42) {
console.log("Non-zero numbers are truthy");
}
if (0) {
console.log("This will not run, 0 is falsy");
} else {
console.log("Zero is falsy");
}
if ("") {
console.log("This will not run, empty string is falsy");
} else {
console.log("Empty string is falsy");
}
if ({}) {
console.log("Empty objects are truthy");
}
if ([]) {
console.log("Empty arrays are truthy");
}
Dangerous and Common Gotchas
// Checking for existence with falsy values
const count = 0;
// Bug: 0 is falsy but is a valid value
if (count) {
console.log(`Count is ${count}`);
} else {
console.log("Count doesn't exist"); // This runs incorrectly
}
// Better approach
if (count !== undefined && count !== null) {
console.log(`Count is ${count}`);
} else {
console.log("Count doesn't exist");
}
// Or using nullish coalescing
const displayCount = count ?? "Count doesn't exist";
console.log(displayCount); // Shows 0, not the fallback
// Another common mistake with objects
const user = { name: "", active: false };
// Bug: Checking falsy values incorrectly
if (!user.name) {
console.log("Username is required"); // Triggers incorrectly for empty string
}
if (!user.active) {
console.log("Account is inactive"); // This is actually correct for boolean
}
// Better approach
if (user.name === "") {
console.log("Username is required");
}
if (user.active === false) {
console.log("Account is inactive");
}
The Short-Circuit Pattern
Logical operators are commonly used to create concise conditional expressions.
// Logical AND for conditional execution
function greetUser(user) {
// Only calls getName if user exists (truthy)
user && console.log(`Hello, ${user.getName()}`);
}
// Logical OR for default values
function getDisplayName(user) {
// If user.name is falsy, use "Guest" instead
return user.name || "Guest";
}
// Nullish coalescing for real defaults
function getConfigValue(config) {
// Only uses default if config.timeout is null or undefined
// (allows 0 as a valid value)
return config.timeout ?? 30000;
}
// Common pattern: AND + OR
function processUser(user) {
// If user exists, return user.name, otherwise return "Guest"
return user && user.name || "Guest";
}
// Beware of precedence issues
// This is equivalent to: (x && y) || z
const result = x && y || z;
// Use parentheses for clarity
const result2 = x && (y || z); // Different behavior
Analogy: Short-Circuit Evaluation as Electrical Circuits
Logical operators in JavaScript work like electrical circuit branches:
- Logical AND (&&) is like switches in series – if any switch is off (falsy), the current stops flowing, and we get that falsy value. Otherwise, we get the last value.
- Logical OR (||) is like switches in parallel – as soon as one switch is on (truthy), the current flows through that branch, and we get that truthy value. If all are off, we get the last value.
This is why we call it "short-circuit" evaluation – just like electricity takes the path of least resistance, JavaScript stops evaluating as soon as it knows the final result.
The Object-to-Primitive Conversion
When an object is used in a context that requires a primitive value, JavaScript automatically converts it to a primitive.
The ToPrimitive Algorithm
JavaScript follows these steps to convert an object to a primitive:
- If the object has a
Symbol.toPrimitivemethod, call it with a hint. - Otherwise, call object methods in this order:
- For string hint:
toString()thenvalueOf() - For number hint:
valueOf()thentoString() - For default hint:
valueOf()thentoString()in non-strict mode, like number hint
- For string hint:
- If none of these methods returns a primitive, throw a TypeError.
// Standard object conversion
console.log(String({})); // "[object Object]" (toString)
console.log(Number({})); // NaN (valueOf returns object, then toString gives "[object Object]", converting to NaN)
console.log(String([1, 2, 3])); // "1,2,3" (toString joins elements)
console.log(Number([1])); // 1 (single-element array becomes that number)
console.log(Number([1, 2])); // NaN (multi-element array becomes NaN)
// Date object is special
const date = new Date();
console.log(String(date)); // Current date as string
console.log(Number(date)); // Timestamp (milliseconds since epoch)
console.log(date + 1); // String concatenation, not addition!
// + operator with objects
console.log({} + {}); // "[object Object][object Object]"
console.log([1, 2] + [3, 4]); // "1,23,4"
console.log([1, 2] + 3); // "1,23"
Customizing Object-to-Primitive Conversion
// Custom object with toString and valueOf
const myObject = {
value: 42,
toString() {
return `[Custom Object ${this.value}]`;
},
valueOf() {
return this.value;
}
};
console.log(String(myObject)); // "[Custom Object 42]" (toString)
console.log(Number(myObject)); // 42 (valueOf)
console.log(myObject + 1); // 43 (uses valueOf for default hint)
console.log(`${myObject}`); // "[Custom Object 42]" (string hint uses toString)
// Using Symbol.toPrimitive
const advancedObject = {
name: "Special",
value: 100,
// Define custom conversion behavior
[Symbol.toPrimitive](hint) {
console.log(`Converting to ${hint}`);
switch (hint) {
case 'number':
return this.value;
case 'string':
return this.name;
default: // 'default' hint
return `${this.name}:${this.value}`;
}
}
};
console.log(String(advancedObject)); // "Special" (hint: string)
console.log(Number(advancedObject)); // 100 (hint: number)
console.log(advancedObject + "!"); // "Special:100!" (hint: default)
Real-World Example: Custom Money Object
You can create objects that behave naturally in different contexts:
// Create a custom money object with proper conversions
class Money {
constructor(amount, currency = 'USD') {
this.amount = amount;
this.currency = currency;
}
// For string contexts (like string concatenation or display)
toString() {
return this.currency === 'USD'
? `${this.amount.toFixed(2)}`
: `${this.amount.toFixed(2)} ${this.currency}`;
}
// For numeric contexts (like mathematical operations)
valueOf() {
return this.amount;
}
// Full control over conversion
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return this.toString();
}
if (hint === 'number') {
return this.amount;
}
// Default behavior (used by operators like +)
return this.amount;
}
}
// Usage
const salary = new Money(5000);
const bonus = new Money(1000);
// String context
console.log(`Your salary is ${salary}`); // "Your salary is $5000.00"
// Numeric context
console.log(salary > bonus); // true (5000 > 1000)
console.log(salary + bonus); // 6000 (numeric addition)
// Mixed context (+ operator prefers string with strings)
console.log("Total: " + salary); // "Total: $5000.00" (string conversion)
// Using in calculations
const total = salary + bonus;
console.log(`Total compensation: ${new Money(total)}`); // "Total compensation: $6000.00"
Best Practices to Avoid Coercion Bugs
Type coercion can lead to subtle bugs. Here are guidelines to write more predictable code:
Use Strict Equality
// Prefer === over ==
if (value === 0) {
// Only runs when value is exactly 0, not "", false, etc.
}
// Use strict inequality too
if (value !== null) {
// Only true when value is definitely not null
}
Explicit Type Conversion
// Be explicit about types
const count = Number(userInput); // Explicit number conversion
const isEnabled = Boolean(settings.enabled); // Explicit boolean conversion
const label = String(value); // Explicit string conversion
// Parse numbers carefully
const age = parseInt(userInput, 10); // Always specify the radix (base)
const percentage = parseFloat(userInput);
// Check for NaN after conversion
if (isNaN(age)) {
console.error("Invalid age input");
}
Proper Existence Checks
// Check if a variable exists and isn't null/undefined
if (typeof value !== 'undefined' && value !== null) {
// Safe to use value
}
// Modern approach with nullish coalescing
const effectiveValue = value ?? defaultValue;
// Avoid falsy checks for existence when 0 or "" might be valid
// Bad
if (!count) { /* ... */ }
// Good
if (count === undefined || count === null) { /* ... */ }
Defensive Object Access
// Safe object property access
const userName = (user && user.name) || "Guest";
// Modern approach with optional chaining
const userEmail = user?.contact?.email ?? "No email";
// Type checking before operations
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
return a + b;
}
Consistent Return Types
// Bad: inconsistent return types
function getUserCount(users) {
if (!users) return false; // Boolean
if (users.length === 0) return "No users"; // String
return users.length; // Number
}
// Good: consistent return type with error handling
function getUserCount(users) {
if (!users) throw new Error('Users parameter is required');
return users.length; // Always returns a number
}
// Alternative with default
function getUserCount(users = []) {
return users.length; // Always returns a number
}
Real-World Example: Form Validation
Comprehensive form validation with proper type handling:
// Robust form validation
function validateForm(formData) {
const errors = {};
// Name validation - require non-empty string
if (typeof formData.name !== 'string' || formData.name.trim() === '') {
errors.name = "Name is required";
}
// Age validation - require positive number
if (formData.age !== undefined) {
const age = Number(formData.age);
if (isNaN(age) || age <= 0) {
errors.age = "Age must be a positive number";
}
}
// Email validation - proper format
if (typeof formData.email === 'string') {
// Simple regex for email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
errors.email = "Invalid email format";
}
} else if (formData.email !== undefined) {
errors.email = "Email must be a string";
}
// Options must be array of strings
if (formData.options !== undefined) {
if (!Array.isArray(formData.options)) {
errors.options = "Options must be an array";
} else {
const invalidOptions = formData.options.filter(
opt => typeof opt !== 'string'
);
if (invalidOptions.length > 0) {
errors.options = "All options must be strings";
}
}
}
// Return validation result
return {
isValid: Object.keys(errors).length === 0,
errors: errors
};
}
// Test the validation
const formData = {
name: "John Doe",
age: "30", // String that needs conversion
email: "john@example.com",
options: ["option1", 2] // Mixed types
};
console.log(validateForm(formData));
Historical Context and Type Coercion
JavaScript's type coercion behaviors were designed in the early days of the web with different priorities.
Why JavaScript Has Implicit Coercion
- Web Form Processing: Originally designed to handle form input, which comes as strings
- Simplicity for Non-Programmers: Created to be accessible to HTML authors
- Fast Development: Less strict typing means faster prototyping
- Design Decisions: Some decisions were made in 10 days when JavaScript was created
Evolution of Best Practices
// Old JavaScript style (pre-2009)
var result = 0;
for (var i = 0; i < someArray.length; i++) {
if (someArray[i] == true) { // Loose equality
result = result + someArray[i]; // Implicit coercion
}
}
// Modern JavaScript style
let result = 0;
for (const value of someArray) {
if (value === true) { // Strict equality
result += Number(value); // Explicit conversion
}
}
TypeScript and Type Safety
TypeScript was created to address JavaScript's loose typing and coercion issues:
// TypeScript example
function add(a: number, b: number): number {
return a + b; // No need to worry about string concatenation
}
// This would cause a compilation error
add("5", 10); // Error: Argument of type 'string' is not
// assignable to parameter of type 'number'.
Analogy: JavaScript Coercion as Auto-Correct
JavaScript's type coercion is like text auto-correct on your phone:
- It tries to guess what you meant, which is helpful when its guess is right
- It can sometimes change your meaning completely in unpredictable ways
- Experienced users often prefer to be explicit and precise to avoid surprises
- Despite its flaws, it makes things accessible to casual users
Practical Exercise
Let's practice identifying and fixing type coercion issues with exercises of increasing complexity.
Exercise 1: Identify the Bugs
Examine this code and identify the bugs caused by type coercion:
// This function has type coercion bugs
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
// Bug 1: price could be a string
total = total + items[i].price;
}
// Bug 2: Using == allows unexpected values
if (total == 100) {
applyDiscount(total, 10);
}
// Bug 3: Using + with potential string
return "$" + total;
}
// Fixed version
function calculateTotalFixed(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
// Fix 1: Ensure price is a number
total = total + Number(items[i].price);
}
// Fix 2: Use strict equality
if (total === 100) {
applyDiscount(total, 10);
}
// Fix 3: Format properly
return "$" + total.toFixed(2);
}
Exercise 2: Type Safe API Response Handling
Create a function that safely processes an API response:
// API response handler with type safety
function processApiResponse(response) {
// Ensure the response exists
if (!response) {
return { error: "No response received" };
}
// Process status
const status = typeof response.status === 'number'
? response.status
: response.status === undefined
? 404 // Default if missing
: Number(response.status);
// Check if status indicates success
const isSuccess = status >= 200 && status < 300;
// Process data
if (isSuccess) {
// Create a safe user object
if (!response.data || typeof response.data !== 'object') {
return { error: "Invalid data format" };
}
const user = {
id: typeof response.data.id === 'number'
? response.data.id
: typeof response.data.id === 'string'
? parseInt(response.data.id, 10)
: null,
name: typeof response.data.name === 'string'
? response.data.name.trim()
: "Unknown",
isActive: response.data.isActive === true,
// Parse roles array safely
roles: Array.isArray(response.data.roles)
? response.data.roles.filter(role => typeof role === 'string')
: []
};
return { success: true, user };
} else {
// Error handling
return {
success: false,
error: typeof response.error === 'string'
? response.error
: `Error with status code: ${status}`
};
}
}
// Test with different responses
const responses = [
{ status: 200, data: { id: 123, name: "John Doe", isActive: true, roles: ["user", "admin"] } },
{ status: "404", error: "User not found" },
{ status: 200, data: { id: "456", name: 42, isActive: 1, roles: "admin" } },
null
];
responses.forEach((response, index) => {
console.log(`Response ${index + 1}:`, processApiResponse(response));
});
Exercise 3: Advanced Type Coercion Challenge
Create objects with custom conversion behavior:
// Create a Temperature class with appropriate conversions
class Temperature {
constructor(value, unit = 'C') {
this.value = Number(value);
if (isNaN(this.value)) {
throw new Error('Temperature value must be a number');
}
if (unit !== 'C' && unit !== 'F') {
throw new Error('Unit must be C or F');
}
this.unit = unit;
}
// Get temperature in Celsius
toCelsius() {
if (this.unit === 'C') {
return this.value;
}
return (this.value - 32) * 5/9;
}
// Get temperature in Fahrenheit
toFahrenheit() {
if (this.unit === 'F') {
return this.value;
}
return (this.value * 9/5) + 32;
}
// Convert to string
toString() {
return `${this.value}°${this.unit}`;
}
// Convert to number (always in Celsius)
valueOf() {
return this.toCelsius();
}
// Customized primitive conversion
[Symbol.toPrimitive](hint) {
if (hint === 'string') {
return this.toString();
}
if (hint === 'number') {
return this.toCelsius();
}
return this.toCelsius();
}
}
// Test the Temperature class
const boiling = new Temperature(100, 'C');
const freezing = new Temperature(32, 'F');
const room = new Temperature(22, 'C');
// Test string conversion
console.log(`Water boils at ${boiling}`); // "Water boils at 100°C"
// Test numeric operations
console.log(boiling > freezing); // true (100°C > 0°C)
console.log(room + 5); // 27 (22°C + 5)
// Create array and sort by temperature
const temperatures = [boiling, room, freezing];
temperatures.sort((a, b) => a - b); // Sorts based on Celsius values
console.log(temperatures.map(String)); // From coldest to warmest
Key Takeaways
- JavaScript performs both explicit (manual) type conversion and implicit (automatic) type coercion
- Explicit conversion is done with constructor functions like
String(),Number(), andBoolean() - Implicit coercion happens automatically in mixed-type operations, often causing unexpected results
- The
==operator performs type coercion, while===strictly compares without conversion - Objects are converted to primitives using
valueOf(),toString(), or a customSymbol.toPrimitivemethod - Logical operators (
&&,||,??) use truthy/falsy evaluations but return the actual operand values - Best practices include using strict equality, explicit conversions, and defensive programming
- Understanding coercion rules helps you write more predictable JavaScript code