Avoid Object wrapper Types in Typescript—Here's why

Avoid Object wrapper Types in Typescript—Here's why

Consider the following contrived Typescript example:

// 1
const hello = (v: String) => {
  console.log(v)
}

// 2
const hello = (v: string) => {
  console.log(v)
}

Note the difference in the type of the v parameter: string vs String.

The primitive vs object wrapper type
The primitive vs object wrapper type

Don’t use the Object wrapper types

As a general rule, you should rarely use the object wrapper types: Number, String, Boolean, Symbol or Object

This is explicitly stated in the Typescript docs.

https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types
https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types

The question this article aims to answer is: why shouldn’t you?

It’s not enough to follow an instruction without understand why it is advised.

Let’s explore this.

The fundamental difference

My initial thoughts when I first stumbled on this rule was that it had to do with the type system within Typescript.

However, that’s not the case.

The real reason lies in Javascript itself.

Standard Javascript has seven primitive values types: string, boolean, null, undefined, symbol and bigint.

As you may be aware of, all Javascript primitives are rightly represented in the Typescript type system.

So, all of these are valid:

// string
const hello = (v: string) => {
  console.log(v)
}

// boolean 
const hello = (v: boolean) => {
  console.log(v)
}

// null
const hello = (v: null) => {
  console.log(v)
}

// undefined
const hello = (v: undefined) => {
  console.log(v)
}

// symbol
const hello = (v: symbol) => {
  console.log(v)
}

// bigint
const hello = (v: bigint) => {
  console.log(v)
}

On the other hand, standard Javascript also has associating objects for most primitives, e.g., String to represent and manipulate a sequence of characters.

// primitive 
const string = "Hello world" 

// String object 
const string2 = new String("Hello world")
Creating a string with the primitive and String constructor
Creating a string with the primitive and String constructor

Understanding why you shouldn’t use these object wrapper types in Typescript really boils down to you understanding the difference in standard Javascript.

For example, primitives technically do not have methods and are immutable.

However, you can call a method on a string primitive as shown below:

"Hello world".toLowerCase()

If primitives do not have methods, then this should NOT be possible.

That is partly true.

Yes, the primitive has no methods, but Javascript defines a String object type that does.

So, what really happens internally is Javascript converts between the primitive and object.

Javascript freely converts between the primitive and Object
Javascript freely converts between the primitive and Object

When you call toLowerCase on the primitive, Javascript wraps it in a String object, calls the toLowerCase method, and then throws the object away.

An interesting consequence that explains is when you assign a property to a primitive.

For example:

const greeting = "Hola Mundo" // primitive
greeting.language = "Spanish" // assign a property to the primitive
Assigning a property to a primitive
Assigning a property to a primitive

You’ll get no errors when you do this.

Everything seems fine, but is it?

Attempt to access the language property afterwards, e.g.:

greeting.language
// undefined

And you get undefined.

Where did the language property go?

Undefined property upon retrieval
Undefined property upon retrieval

You already know the explanation here.

When you set the language property, the primitive was converted to the String object and the language property set on that object. Then, the object (and its language property) discarded!

Why do these object wrapper types exist?

I have explained so far with the String object wrapper type, but you have object wrapper types for other primitive as well (apart from null and undefined)

These wrapper types mostly exist for convenience. They provide methods on the primitive values and, perhaps more importantly (in real usage), they provide static methods such as String.fromCharCode.

Other than these, there’s typically no reason you’d want to use these, EVEN IN YOUR JAVASCRIPT CODE.

The Typescript distinction

Just as the object wrapper types differ from their primitives, Typescript models this distinction, i.e., providing types for both primitives and their object wrappers.

Essentially, you have the string and String type, number and Number type, etc.

If you’re coming from some other programming language such as Java you may find yourself using the Object wrapper types e.g.:

const hello = (v: String) => {
  console.log(v)
}

This seems to work fine:

const hello = (v: String) => {
  console.log(v)
}

hello("world!")

hello(new String("world!"))

But you’ll run into issues when you start to use Javascript methods that expect a primitive string e.g.,

const isValidHello = (v: String) => {
  console.log(["Hello"].includes(v)) // error here
}
Error: the includes method expects a string primitive
Error: the includes method expects a string primitive

Typescript playground

string is assignable to String, but String is not assignable to string 🤯

Save yourself the hassle and stick to the primitive types.

Most type declarations that ship with Typescript use it, you should do the same.

Conclusion

Typescript is a superset of Javascript. It’s only fair that it models the underlying Javascript system.

The object wrapper types exist for completeness and in the rare case that you actually need them. Otherwise, don’t use them. Stick to the primitive types.

Cheers 🎉

Fancy some free Typescript resources?

Download the cheatsheet

Get the PDF or ePub in exchange you sign up for my newsletter. No spam, ever. Unsubscribe at any time.

Build strongly typed Polymorphic React components book

Get this book for free in exchange you sign up for my newsletter. No spam, ever. Unsubscribe at any time.