Converting an arbitrary value to a string in JavaScript is surprisingly nuanced. There are 3 common ways to convert v to a string:

v.toString();
'' + v;
String(v);

Each of the 3 methods have tradeoffs and quirks. For example, if v is null or undefined:

const v = null;

'' + v; // 'null'
String(v); // 'null'
v.toString(); // Throws a TypeError

For another, more complex, example, consider the following object.

const v = { valueOf: () => null };

What do you get when you try to convert this object to a string?

v.toString(); // '[object Object]'
'' + v; // 'null'
String(v); // '[object Object]'

That's because '' + v and String(v) are almost equivalent, except for how they handle valueOf(). String(v) doesn't attempt to convert v to a primitive before converting the value to a string, which is generally safer.

const v = { valueOf: () => { throw Error('Oops!') } };

v.toString(); // '[object Object]'
String(v); // '[object Object]'
'' + v; // Throws an error

Using Archetype

The nuance of what happens when you're converting one value to a string is confusing enough. What happens when you have an embedded object with multiple arrays of strings that you need to convert? After all, recursion is hard.

I wrote Archetype to specifically address the problem of validating and casting complex JSON objects. Archetype is a full schema validation library, but it also exposes a neat to() function for converting individual values. Archetype calls to() internally.

const Archetype = require('archetype');

// Archetype refuses to cast POJOs to strings because generally this means you
// got bad JSON data from a network request.
Archetype.to({ valueOf: () => null }, 'string'); // Throws an error
Archetype.to({ valueOf: () => '42' }, 'string'); // Throws an error

// But dates, etc. are ok:
Archetype.to(new Date(), 'string'); // 'Tue Feb 12 2019 09:24:55 GMT-0500 (EST)'
Archetype.to(42, 'string'); // '42'
Archetype.to(Number(42), '42');

Archetype doesn't convert null or undefined ("nullish" values) to a string. That's intentional because when you get a null value in a request body, you usually want to treat it as null rather than as a string.

Archetype.to(null, 'string'); // null
Archetype.to(undefined, 'string'); // undefined

Complex Objects

Where Archetype really shines is casting complex objects that contain string-like values using the same rules as Archetype.to(). For example, suppose you have this complex object:

const obj = {
  tags: [false, 42],
  comments: [
    {
      time: new Date(),
      body: 'Hello'
    }
  ]
};

Archetype can cast and validate all these deeply nested properties into strings:

const Comment = new Archetype({
  time: 'string',
  body: 'string'
}).compile('Comment');

const Type = new Archetype({
  tags: ['string'],
  comments: [Comment]
});

new Type(obj); // `tags`, `comments.time`, `comments.body` are now all strings

Moving On

To convert a value v to a string, you should generally use String(v), because String(v) doesn't throw if v is nullish and doesn't convert objects that have a valueOf() function. If you're looking to convert a value coming in from an HTTP request, you should consider using Archetype. Archetype is designed to handle the nasty surprises that can happen when converting JSON data to strings or numbers, so you can handle HTTP requests and responses with confidence.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus