A day ago I asked my developers friends:
How to know a JavaScript object literal type?
Most of the answers of the answers suggested to use instanceof, while this answer works for objects created by using a constructor, it can not work with objects literals or maybe for some reasons developers don't export the object constructor from the module but they provide a factory with object initializer instead.
The use case:
The question was about the following use case:
const user = {
username: 'johndoe',
firstname: 'John',
lastname: 'Doe'
};
It is hard to determine if the object above is in certain type without checking the presence of all the properties, imagine if we have a more than 10 properties or more, it would be exhausting, right?
It is obvious it would be good if we can know that the object is indeed in User
type just by a single call.
Using Object.prototype.toString()
gives the object type inside square brackets for the built-in types/objects:
console.log(Object.prototype.toString.call(1)); // -> '[object Number]'
console.log(Object.prototype.toString.call('Cherif')); // -> '[object String]'
Or:
const one = 1;
const name = 'Cherif';
console.log(one.toString()); // -> '[object Number]'
console.log(name.toString()); // -> '[object String]'
But for a developer defined objects it gives a [object Object]
result, a generic object:
const user = {
username: 'johndoe',
firstname: 'John',
lastname: 'Doe'
};
console.log(user.toString()); // -> '[object Object]'
console.log(Object.prototype.toString.call(user)); // -> '[object Object]'
Symbol.toStringTag to the rescue
Fortunately with the introduction of ES6 Symbol
s a number of built-in symbols had seen the day make it possible to describe custom objects type by overriding Object.prototype.toString()
using Symbol.toStringTag
, so to describe a type for our user
object we can do the following:
const user = {
username: 'johndoe',
firstname: 'John',
lastname: 'Doe',
get [Symbol.toStringTag]() {
return 'User'; // The string tag description
}
};
console.log(user.toString(user)); // -> '[object User]'
console.log(Object.prototype.toString.call(user)); // -> '[object User]'
Read the string using Symbol.toStringTag:
I would advice to make the check using the Symbol
property by using user[Symbol.toStringTag]
, like this we have the possibility to override toString
function for other purposes, and we can just have User
string from the string tag instead of [object User]
example:
const user = {
username: 'johndoe',
firstname: 'John',
lastname: 'Doe',
get [Symbol.toStringTag]() {
return 'User'; // The string tag description
},
toString() {
return `${this.firstname} ${this.lastname}`;
}
};
console.log(user[Symbol.toStringTag]); // -> "User"
console.log(user.toString()); // -> "John Doe"
All the code examples are in the following CodePen:
See the Pen Symbol.stringTag by Mohamed Cherif Bouchelaghem
(@cherifGsoul) on CodePen.
I hope you find this article useful and helpful, let me know your thoughts in the comments below.
Top comments (12)
Nice, I didn't know about this!
But I wouldn't trust this trick for type-safety checking
whereas in TypeScript, any operation changing the shape of an object outside of its type is prevented
It's important not to forget that, at the end of the day, TypeScript isn't anything more than code sugar, pre-compilation. Post-compilation (and subsequent runtime), it's all regular ole' JavaScript. Nothing more. Nothing less. Enforcing "Type" safety in any kind of "strict" manner, will always, always, always only go so far. ECMAScript Primitives are the only datums which, natively, guarantee preserving "Type"(Undefined, Null, String, Boolean, Number, and Symbol). All remaining ECMAScript types/values fall under "Object Type" classifications/sub-classes. Why is any of this important? Objects, (be it a "POJO", function, constructor, exotic, emoji, whatever), by their very nature in JavaScript land are not ever going to be "secure", ESPECIALLY in terms of structural exposition/composition (i.e. "interface" or "shape"). However, we CAN secure "Object Type" checks and guards that remain true at runtime by placing the intended "Type" enforcements around the instance itself.
Totally agreed! Thank you!
This compiles just fine in TypeScript and even gives an error in the playground when run.
Sure, but come on, that's just deliberately trying to deceive TypeScript at this point ^^
I agree, however there's other ways to prevent this like freezing or using Proxies.
Very interesting. How I wish this was a built-in feature.
It is built-in you just need to override the Symbol.
No I mean built-in as in you don't have to code anything else, just do a
user.toStringTag()
and it'll return[object user]
.thank you 🙏🏻
Very useful. Thanks!
interesting, ready i like the way how did you explain it, thnaks