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
.
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.
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")
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.
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
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?
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
}
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?
Get the PDF or ePub in exchange you sign up for my newsletter. No spam, ever. Unsubscribe at any time.
Get this book for free in exchange you sign up for my newsletter. No spam, ever. Unsubscribe at any time.