DEV Community

Cover image for 'string' vs 'String': The Battle Between Primitive and Object Types in TypeScript
Stephen Akugbe
Stephen Akugbe

Posted on

'string' vs 'String': The Battle Between Primitive and Object Types in TypeScript

In TypeScript, types can be categorized into primitive and reference types (or objects). Understanding the distinction between these types is critical for writing efficient and predictable code. This article dives deep into the differences between primitives and their object counterparts, shedding light on common pitfalls and best practices.

What Are Primitive Types?
Primitive types are the most basic data types in TypeScript. They represent simple, immutable values. When working with primitives, you deal directly with the value itself, not an object.

The primitive types in TypeScript include:

boolean
number
string
null
undefined
symbol
bigint

These values are immutable, meaning their data cannot be modified and are not objects. Because of this, they are more memory-efficient and typically faster than objects.

let isComplete: boolean = true;
let age: number = 25;
let name: string = "John Doe";
Enter fullscreen mode Exit fullscreen mode

What Are Wrapper Object Types?
Wrapper object types are reference types that "wrap" around the primitive types. These are constructed using the new keyword, creating an object that has methods and properties associated with the primitive value.

For example:

Boolean is the wrapper for boolean
Number is the wrapper for number
String is the wrapper for string
BigInt is the wrapper for bigint

These objects allow you to work with the primitive values as objects, which can be useful in some cases but introduces unnecessary complexity in most.

let isComplete: Boolean = new Boolean(true);
let age: Number = new Number(25);
let name: String = new String("John Doe");
Enter fullscreen mode Exit fullscreen mode

While it might seem like a small difference, using wrapper objects instead of primitives can lead to unexpected behavior and inefficiencies.

Key Differences Between Primitives and Wrappers

  1. Memory Efficiency: Primitives are stored in the stack, making them lightweight and faster to access.
    Objects are stored in the heap, which can consume more memory and are slower due to reference-based operations.

  2. Behavior in Logical Comparisons: Objects (including wrapper objects) are inherently "truthy" in JavaScript, which can cause unexpected behavior in conditionals.

const isComplete: Boolean = new Boolean(false);
if (isComplete) {
  console.log("This will execute, because objects are truthy!"); // This runs
}
Enter fullscreen mode Exit fullscreen mode

This behavior differs from the primitive boolean type, which behaves intuitively in logical expressions.

  1. Comparison Pitfalls: Primitives are compared by value, meaning two primitives are equal if they have the same value. Wrapper objects are compared by reference, meaning two objects are equal only if they refer to the exact same object in memory.
let str1: string = "hello";
let str2: String = new String("hello");

console.log(str1 === str2); // Evaluates to false, because str2 is an object
Enter fullscreen mode Exit fullscreen mode
  1. Methods: Primitive values do not have methods or properties directly but are automatically coerced to their object equivalents when needed, allowing methods like .toUpperCase() on strings. Wrapper objects come with a full set of methods and properties, but they are less commonly needed in day-to-day programming.

Common Pitfalls in TypeScript

  1. boolean vs Boolean
let isActive: boolean = true; // Primitive
let isActiveObj: Boolean = new Boolean(true); // Wrapper object

if (isActiveObj) {
  console.log("This will run even if the value is false!"); // Objects are truthy
}
Enter fullscreen mode Exit fullscreen mode
  • Primitive: Use when you need a true/false value.
  • Wrapper Object: Avoid using unless absolutely necessary.
  1. number vs Number
let age: number = 42;        // Primitive
let ageObj: Number = new Number(42);  // Wrapper Object
Enter fullscreen mode Exit fullscreen mode

Using Number introduces an unnecessary object, making your program slower and more memory-intensive.

  1. string vs String
let name: string = "Jane Doe";   // Primitive
let nameObj: String = new String("Jane Doe");  // Wrapper Object

console.log(name === nameObj);  // false
Enter fullscreen mode Exit fullscreen mode
  • A primitive string and a String object can lead to false equality checks, so it's recommended to stick with the primitive.
  • bigint vs BigInt
let largeNumber: bigint = 12345678901234567890n;  // Primitive
let largeNumberObj: BigInt = Object(12345678901234567890n);  // Wrapper Object
Enter fullscreen mode Exit fullscreen mode
  • With bigint, similar rules apply. The wrapper BigInt should generally be avoided in favor of the primitive.

Best Practices

  1. Use Primitive Types by Default: Primitives are faster, more efficient, and behave as expected in most cases. Stick with primitives unless there's a compelling reason to use wrapper objects.

  2. Avoid Wrapper Objects: Wrapper objects (Boolean, Number, String, etc.) are rarely needed in modern development and can introduce unexpected behavior. Avoid using new with these types.

  3. Use TypeScript's Type Annotations: Explicitly annotate your variables with the primitive types (boolean, string, number, etc.) to ensure consistency and avoid unintended type coercion.

let isActive: boolean = true;  // Good practice
Enter fullscreen mode Exit fullscreen mode
  1. Pay Attention to Conditional Logic: Wrapper objects can be truthy even if their underlying value is falsy. When using conditionals, always check if you're dealing with a primitive or a wrapper.

Conclusion
Primitive types and their wrapper object equivalents may seem interchangeable at first, but they have important differences in TypeScript. While wrapper objects have their uses, they are generally less efficient and can lead to unexpected behavior. In most cases, it's best to use primitive types (boolean, string, number, etc.) for more predictable, efficient, and cleaner code.

By understanding these distinctions and avoiding common pitfalls, you’ll be able to write more robust TypeScript applications that behave as expected.

Don't forget to like this article if you found it helpful.

Top comments (0)