The Hidden World of Wrapper Objects
JavaScript's primitive types--string, number, boolean, bigint, symbol, and undefined--are not objects. Yet they behave like objects when you access methods on them. When you write "hello".toUpperCase(), JavaScript temporarily wraps the primitive string in a String object, calls the method, then discards the wrapper. This process is called autoboxing or primitive boxing.
Autoboxing is fundamental to JavaScript's design. Every primitive type except null and undefined has a corresponding wrapper constructor: String for text, Number for numeric values, Boolean for true/false, BigInt for large integers, and Symbol for unique identifiers. These wrapper constructors exist because primitives cannot have methods or properties directly--but JavaScript makes it appear as though they do through this temporary wrapping mechanism.
The confusion arises when developers explicitly create these wrapper objects using the new keyword. While the language supports new String(), new Number(), and new Boolean(), doing so creates persistent objects rather than temporary wrappers. This distinction leads to unexpected behavior in type checking, equality comparisons, and conditional logic. Understanding this difference is essential for writing predictable JavaScript code and avoiding subtle bugs that can be difficult to diagnose. For teams building robust web applications, following JavaScript best practices helps prevent these issues from entering your codebase.
The 8 JavaScript Data Types
- Primitives: Undefined, null, boolean, number, string, bigint, symbol
- Reference type: Object (including functions and arrays)
- Each primitive type has a corresponding wrapper constructor
- Autoboxing enables methods on primitive values without explicit object creation
The key takeaway is that while wrapper objects exist for technical reasons, you should almost never create them explicitly in your code. Use string literals ("text"), number literals (42), and boolean literals (true/false) instead.
1// How autoboxing works2const text = "hello";3const upper = text.toUpperCase(); // String object created, method called, object discarded4 5// Same result without explicit object creation6console.log(upper); // "HELLO"7 8// The wrapper object is temporary and immediately garbage collected9// You never need to create it explicitlyThe typeof Trap
The most immediately visible problem with wrapper objects is their typeof behavior. When you create an object with new String(), JavaScript's typeof operator returns "object"--not "string" as you might expect. This happens because the new keyword explicitly creates an object instance, and all objects (regardless of what they wrap) return "object" from typeof.
This seemingly simple difference can cause significant problems in your code. Type checks that expect strings might fail silently when they receive String objects. Conditional logic that relies on type information can behave unexpectedly. Even worse, these issues might only manifest in specific edge cases, making them difficult to track down during testing.
The ESLint no-new-wrappers rule exists specifically to prevent this confusion by catching wrapper object creation at development time. When you use primitive literals, typeof returns exactly what you expect: "string" for text, "number" for numeric values, and "boolean" for true/false. This predictability is essential for writing reliable JavaScript applications.
According to the MDN documentation on JavaScript data structures, understanding the distinction between primitives and objects is fundamental to mastering the language. The documentation explicitly recommends using primitives over wrapper objects for this reason.
1// The typeof trap2const stringObject = new String("hello");3console.log(typeof stringObject); // "object"4 5const text = "hello";6console.log(typeof text); // "string"7 8// This affects type guards and conditional logic9if (typeof maybeString === "string") {10 // With primitives, this works as expected11 console.log(maybeString.length); // Safe to access12}13 14// With wrapper objects, type checks fail15const obj = new String("test");16if (typeof obj === "string") { // FALSE!17 // This code never executes18}Reference Equality Problems
Primitive strings use value-based equality, but String objects use reference equality. Two String objects containing identical values are not considered equal because they are distinct objects in memory. This means that even if you create two String objects with the same content, comparing them with === or == will return false.
This behavior can silently break caching, memoization, and comparison logic throughout your codebase. If you're using String objects as keys in a Map or checking for duplicate values, you might incorrectly conclude that identical strings are different. The same issue applies to Number and Boolean wrapper objects.
With primitive strings, JavaScript performs value-based comparison. Two primitive strings with the same content are considered equal because JavaScript compares the actual values, not memory addresses. This is the behavior you expect and want in virtually every situation. The primitive approach also enables string interning, where identical string literals share memory addresses for efficiency.
The difference becomes particularly problematic when code passes objects between functions or modules. One part of your codebase might create a String object while another expects a primitive, leading to unexpected equality failures that are difficult to trace. Sticking with primitives eliminates this entire class of bugs and makes your web development workflow more reliable.
1// Reference vs value equality2const a = new String("hello");3const b = new String("hello");4 5console.log(a === b); // false - different object references6console.log(a == b); // false - different object references7 8const x = "hello";9const y = "hello";10console.log(x === y); // true - primitive value comparison11 12// This breaks Set, Map, and object key lookups13const seen = new Set();14seen.add(new String("test")); // Object added15seen.add(new String("test")); // Different object, added again!16console.log(seen.size); // 2, not 117 18// With primitives, Set works correctly19const primitiveSet = new Set();20primitiveSet.add("test");21primitiveSet.add("test");22console.log(primitiveSet.size); // 1The Truthiness Trap with Boolean Objects
The Boolean wrapper creates the most dangerous runtime behavior because all objects are truthy--even those created with false. In JavaScript, any object (including Boolean wrapper objects) evaluates to true in conditional contexts, regardless of what value it contains. This fundamentally breaks boolean logic and can introduce subtle, hard-to-detect bugs in your application.
When you write if (someValue), you're asking whether the value is truthy. For boolean primitives, this works as expected: if (true) executes, and if (false) does not. However, if (new Boolean(false)) always executes because the Boolean object itself is an object, and all objects are truthy.
This issue is particularly insidious because the code appears correct. The Boolean object even has a valueOf() method that returns the wrapped boolean value. Code might check object.valueOf() === false and get the expected result, but forgetting to call valueOf() leads to incorrect behavior. This kind of bug can slip through code reviews and testing, only manifesting in production when specific conditions are met.
The ESLint documentation specifically highlights Boolean objects as dangerous because they break the fundamental expectation that false represents falseness. Always use boolean primitives true and false in your conditionals. Implementing proper code quality standards helps catch these issues early.
1// The Boolean truthiness trap2const falseObject = new Boolean(false);3const trueObject = new Boolean(true);4 5if (falseObject) {6 console.log("This code runs!"); // Always executes7}8 9if (trueObject) {10 console.log("This also runs"); // Also executes11}12 13if (falseObject.valueOf() === false) {14 console.log("valueOf() returns false"); // This is true15}16 17// This means code like this is BROKEN:18function checkAccess(user) {19 if (!user.hasAccess) {20 throw new Error("Access denied");21 }22 // Grant access...23}24 25// If someone passes a Boolean object:26const brokenUser = { hasAccess: new Boolean(false) };27checkAccess(brokenUser); // Does NOT throw - bug!1// Problematic code with Boolean objects2function processUser(user) {3 if (!user.isActive) {4 return "User is inactive";5 }6 return "Processing user...";7}8 9// Someone passes a Boolean object instead of a boolean10const user = { isActive: new Boolean(false) };11 12processUser(user); // Returns "Processing user..." - WRONG!13 14// The correct way - use boolean primitives15const correctUser = { isActive: false };16processUser(correctUser); // Returns "User is inactive"17 18// Type guards can help catch this in TypeScript19function isBoolean(value: unknown): value is boolean {20 return typeof value === "boolean";21}The TypeScript Type Confusion
TypeScript defines confusing pairs of types that look similar but behave differently: boolean/Boolean, number/Number, string/String, bigint/BigInt, symbol/Symbol, and object/Object. The lowercase versions represent primitives, while uppercase versions represent wrapper object types. Understanding this distinction is crucial for writing idiomatic TypeScript code.
The uppercase types are technically valid TypeScript types, and primitive values are assignable to them due to TypeScript's structural typing system. However, using uppercase types in your type declarations is not idiomatic and can lead to confusion. The TypeScript-ESLint rule no-wrapper-object-types explicitly recommends against this practice.
When you declare a variable as String, you're saying "this can be any String object," which includes wrapper objects created with new String(). But since primitives are also assignable to this type through structural typing, you end up with code that accepts both primitives and objects--defeating the purpose of having specific types. This ambiguity makes it harder to reason about your code and can lead to the same problems we discussed with typeof and equality.
The recommended approach is simple: always use lowercase types in TypeScript. Use string instead of String, number instead of Number, and boolean instead of Boolean. This aligns with JavaScript best practices and makes your TypeScript code more predictable. Your linter can enforce this automatically with the no-wrapper-object-types rule. Following TypeScript development best practices ensures your codebase remains maintainable.
1// TypeScript type confusion2let myString: String = "hello"; // Works but not idiomatic3let myNumber: Number = 42; // Works but not idiomatic4let myBoolean: Boolean = true; // Works but not idiomatic5 6// Preferred lowercase types7let betterString: string = "hello"; // Correct8let betterNumber: number = 42; // Correct9let betterBoolean: boolean = true; // Correct10 11// Why uppercase is problematic:12// String accepts both primitive string AND String object13const str: String = new String("wrapped"); // Valid TypeScript14const prim: String = "plain"; // Also valid!15 16// This means type checks don't guarantee primitive behavior17function processText(input: string): number {18 return input.length; // Works with primitives19}20 21// With String type, input might be an object:22function brokenText(input: String): number {23 return input.length; // Still works, but input might be object24}25 26// The fix - use lowercase types everywhere27function correctText(input: string): number {28 return input.length; // Only accepts primitives29}Expected typeof Results
Primitives return the expected type ("string", "number", "boolean") instead of "object"
Correct Truthiness
Boolean primitives correctly evaluate to true or false in conditionals
Value Equality
Primitive comparisons use value equality, making caching and comparison work predictably
Better Performance
No unnecessary object allocation and garbage collection overhead
Clearer Code Intent
Literals communicate intent more clearly than constructor calls
Better Tooling
Linters and TypeScript can easily verify and enforce primitive usage
ESLint Rules for Prevention
Modern JavaScript projects use static analysis tools to catch wrapper constructor usage before it reaches production. ESLint provides robust rules that prevent these mistakes automatically, helping teams maintain code quality without relying solely on code reviews.
no-new-wrappers
This core ESLint rule prevents creating String, Number, and Boolean objects with new. The rule is simple but effective: it flags any use of new String(), new Number(), or new Boolean() and reports an error. This catches mistakes early in development, before they can cause runtime issues.
The rule is part of ESLint's core ruleset, meaning you don't need any plugins to use it. Simply adding "no-new-wrappers": "error" to your ESLint configuration enables protection against wrapper objects. Many project templates and style guides include this rule by default because the cases where you genuinely need wrapper objects are extremely rare.
no-object-constructor
This rule discourages new Object() in favor of object literals {}. While less dangerous than wrapper objects, new Object() is still unnecessary when object literals are available. The object literal syntax is more concise, more idiomatic, and immediately communicates that you're creating a simple object.
For TypeScript projects, the TypeScript-ESLint plugin provides the no-wrapper-object-types rule, which prevents using uppercase wrapper types in type annotations. This rule complements the JavaScript ESLint rules by providing type-level protection against wrapper object usage. Implementing these linting rules as part of your development workflow helps maintain consistent code quality across your team.
1/*eslint no-new-wrappers: "error"*/2 3// These will cause lint errors4const stringObject = new String("hello"); // Error5const numberObject = new Number(42); // Error6const booleanObject = new Boolean(false); // Error7 8// Correct approach - use as functions, not constructors9const text = String(someValue);10const num = Number(someValue);11const bool = Boolean(someValue);12 13// Also covered by no-object-constructor14/*eslint no-object-constructor: "error"*/15const obj = new Object(); // Error - use {} instead1// In eslint.config.mjs or .eslintrc.cjs2export default tseslint.config({3 rules: {4 "@typescript-eslint/no-wrapper-object-types": "error"5 }6});7 8// Incorrect - uppercase types (will error)9let myString: String;10let myNumber: Number;11let myBoolean: Boolean;12 13// Correct - lowercase types14let myString: string;15let myNumber: number;16let myBoolean: boolean;17 18// Function return types should also use lowercase19function createMessage(text: string): string {20 return text.toUpperCase();21}22 23// Not:24function brokenMessage(text: String): String { // Error!25 return text.toUpperCase();26}When Wrapper Constructors Are Actually Useful
While wrapper constructors should generally be avoided, there are rare scenarios where they have legitimate uses. Understanding these edge cases helps you make informed decisions about when (if ever) to deviate from the standard practice.
Type Conversion Without new
The wrapper constructors themselves (without new) serve as type conversion functions. String(value), Number(value), and Boolean(value) convert their arguments to primitives without creating wrapper objects. This is the intended use case for these functions--they're constructors designed to work as standalone functions for type coercion.
This pattern is commonly used for explicit type conversion in JavaScript. Number("42") converts a string to a number, String(true) converts a boolean to a string, and Boolean(1) converts a value to true or false. Unlike the new keyword, calling these as functions returns primitives, not objects. This is the correct way to use the wrapper constructors.
Dynamic Property Access (Legacy)
In very old JavaScript code, you might see String objects used for dynamic property access since objects can have arbitrary properties. However, primitive strings support bracket notation through autoboxing, so this pattern is obsolete. Modern JavaScript handles property access on primitives uniformly.
Extending Wrapper Prototypes (Strongly Discouraged)
Some legacy code extends wrapper prototypes to add custom methods, though this practice is strongly discouraged. Modifying built-in prototypes can conflict with future language features and cause maintenance issues. If you need custom string methods, consider using a utility function or a wrapper library instead of modifying String.prototype.
1// Type conversion functions - these are fine2const num = Number("42"); // 42 (number primitive)3const str = String(42); // "42" (string primitive)4const bool = Boolean(1); // true (boolean primitive)5 6// These are NOT the same as new Wrapper()7const n = new Number("42"); // Number object - AVOID8 9// Object literal is preferred over new Object()10const obj = {}; // Preferred11const alsoObj = new Object(); // Avoid12 13// Dynamic property access works on primitives too14const text = "hello";15console.log(text[0]); // "h" - bracket notation works16console.log(text["length"]); // 5 - properties accessible17 18// If you need custom string methods, use a utility function19function reverseString(str: string): string {20 return str.split('').reverse().join('');21}22 23// Or create a wrapper class (not extending prototype)24class StringUtils {25 static reverse(str: string): string {26 return str.split('').reverse().join('');27 }28}Summary and Best Practices
The rule is simple: never use new with built-in constructors for primitives. This single guideline prevents an entire class of subtle bugs related to type checking, equality comparisons, and boolean logic.
What to Avoid
| Avoid | Use Instead |
|---|---|
new String() | "text" or String(value) |
new Number() | 42 or Number(value) |
new Boolean() | true/false or Boolean(value) |
new Object() | {} |
String type | string type |
Number type | number type |
Boolean type | boolean type |
Recommended ESLint Configuration
// eslint.config.mjs
export default [
{ rules: { "no-new-wrappers": "error" } },
{ rules: { "no-object-constructor": "error" } }
];
// For TypeScript
// Add "@typescript-eslint/no-wrapper-object-types": "error"
By following these guidelines, you'll write cleaner, more predictable JavaScript code that avoids the subtle bugs caused by primitive wrapper objects. The key insight is that JavaScript's autoboxing mechanism already handles the temporary wrapping needed for method access--you never need to create wrapper objects explicitly.
For more on writing clean, bug-free JavaScript, see our guide on TypeScript mixins examples and use cases or explore our JavaScript development services to learn how we can help improve your codebase quality.