Python `__str__()` Equivalent

In Python, we can represent the object returned by a class as a string.

class Person:
  def __init__(self, name):
    self.name = name

  def __str__(self):
    return f"Hello! My name is {self.name}!"

What if we could do something similar in Javascript? This could be used by libraries to describe a class without affecting its methods and properties. It doesn't need to be __str__.

Let me know what you think. Thank you :grinning:.

This is already possible in Javascript today, through the special .toString() function - and many libraries already use it.

For example:

> { toString: () => 'Hello' } + ' World!'
'Hello World!'
> String(new URLSearchParams({ a: 1, b: 2 }))
'a=1&b=2'
1 Like

Hi @theScottyJam,

Sorry, isn't your example limited to an object? How can I do the same with a class?

All instances of classes are just objects, so if it works with an object, it works with a class.

class MyClass {
  toString() {
    return 'Hello'
  }
}

const instance = new MyClass()
console.log(instance + ' World!') // Hello World!

Are you looking for .toString(), or [Symbol.toStringTag], or in node, [util.inspect.custom]()?

a class is an object too. for the constructor, you can have static toString() {}, and for the instances, toString() {}.

Nitpicking here: don't conflate __repr__ with __str__. __repr__ is supposed to return a string that is ideally valid Python code for constructing that object; if that's not possible, at least some pseudo-code form. __str__ on the other hand, is expected to be free-form text.

class Person:
  def __init__(self, name):
    self.name = name
  def __repr__(self):
    return f"{self.__class__.__name__}({self.name!r})"
  def __str__(self):
    return f"Hello! My name is {self.name}!"

@lightmare Sure, I'll change that.

@ljharb Just .toString(). I didn't know you could use that as __str__.

@theScottyJam Your example works fine, but this code...

class Person {
  constructor(name) {
    this.name = name;
  }

  toString() {
    return `Hello ${this.name}!`;
  }
}

...produces this...

const person = new Person("Joe");
console.log(person); // Person { name: 'Joe' }
console.log(person + "Elephant"); // Hello Joe!Elephant

Why doesn't it give me Hello Joe when I log person. Is there anything wrong with my code?

.toString() is what gets automatically called when an object needs to be turned into a string. console.log() doesn't turn things into strings before logging them out (which is why browsers are able to give you an interactive view of objects when you log them out). You can manually do it like this console.log(String(person)).

While toString() should get you what you want 99.9% of the time, going deeper, string conversion can get a little more complicated. There's also the valueOf() and [Symbol.toPrimitive](hint) methods. When converting an object to a primitive (e.g. a string), any one of these methods can potentially be called. Which are called depends on their existence, the kind of conversion being performed, and potentially the value they return. Consider the following using both a toString and valueOf:

class MyClass {
  toString() {
    return 'As string'
  }
  valueOf() {
    return 'As value'
  }
}

console.log('' + new MyClass) // As value
console.log(String(new MyClass)) // As string

When using string concatenation (via +) valueOf() was used, not toString(), however toString() was used with an explicit String() conversion. If we remove the valueOf(), we get toString() used for both.

class MyClass {
  toString() {
    return 'As string'
  }
}

console.log('' + new MyClass) // As string
console.log(String(new MyClass)) // As string

... and this despite objects inheriting a default valueOf() from Object.prototype. So its not that a valueOf() doesn't exist here, rather that the default valueOf() returns this which is an object and not a primitive so the conversion falls back to using toString() instead because it demands a primitive value.

const myObj = new MyClass // the version without its own valueOf()
console.log(myObj === myObj.valueOf()) // true

Which method is used first during conversion depends on what you're doing and if the first doesn't return a primitive, the other is used as a fallback. "What you're doing" is divided into 3 kinds of conversions: "string", "number", and "default". More specifically these are "hints" into how the operation is meant to perform the conversion which becomes more clear with the 3rd method [Symbol.toPrimitive](hint) . If present, this method supersedes all uses of valueOf() and toString() (not using them as fallbacks either) but comes with the benefit of having insight into the conversion hint through its argument.

class MyClass {
  [Symbol.toPrimitive](hint) {
    console.log(`As primitive with hint: ${hint}`)
    return ''
  }
}

'' + new MyClass // As primitive with hint: default
String(new MyClass) // As primitive with hint: string
10 * new MyClass // As primitive with hint: number

In the toString()/valueOf() the "default" and "number" hints go to valueOf() first whereas the "string" hint starts with toString(). When using [Symbol.toPrimitive](hint), they all go through that single method passing in the hint along with it. The biggest advantage here being that you can separate "default" from "number" conversions.

So if you want the most control over string and other primitive conversions, you might want to implement a [Symbol.toPrimitive](hint). Otherwise, you should get by just fine with a toString().

...anyway, theres more than you ever wanted to know about string conversion options for your class.

Bonus trivia: Though Object.prototype doesn't implement [Symbol.toPrimitive](hint), some other builtins do, like Date objects. Dates implement one to reverse the precedence of their "default" toString() and valueOf() calls. For example where normally a valueOf() would be called first for a "" + new Date, instead toString() is.

const date = new Date
console.log(date.toString()) // Wed Jul 21 2021 14:02:46 GMT-0400 (Eastern Daylight Time)
console.log(date.valueOf()) // 1626890566431
console.log("" + date) // Wed Jul 21 2021 14:02:46 GMT-0400 (Eastern Daylight Time)

// remove toPrimitive for default behavior
Object.defineProperty(date, Symbol.toPrimitive, { value: null });
console.log("" + date) // 1626890566431
2 Likes