JavaScript weird type system - Type coercion is bad

Heyaa, welcome back, I hope you're doing well, in the previous article we've discussed basic data types and some discussions around some types, you can check it up if you don't know about that.

But now we're going to talk about the:

Type Coercion

One of the most important and underrated concepts related to types, I'd go saying if you don't understand type coercion and how it works, you won't be able to reason about your code base.

How would you make an algorithm if you're ignoring if types coerce to another type?

That's where bugs come from btw, the divergence of your mental model or intention and how JavaScript behaves and thinks, and yeh, JS never mistakes.. umm, maybe a little bit.. aight, JS messes up with types, so help her!

I don't know about you, but I find these anti-coercion people who claim that type-coercion is evil, are evil themselves.

How could you drop off some built-in feature from the language?!

And hey, that's not some avoidable feature, you're using implicit coercion and maybe you don't know, guess what, how hard you try to avoid coercion and be explicit about your types and cast according to the situation yada yada yada. You still coerce!

JavaScript uses coercion internaly whether you like or not, so why not we embrace this feature instead of avoiding it?

You'd make a better developer of yourself if you're using every possible feature of the language the right way, instead of using some features and ignore some others.

If you're typing more reasonable code to JavaScript you'll have narrowed down the divergence between your mental model and JS intention which results in better code base.

So what's type coercion all about?

Type coerion is changing type from X to Y - Me

Some fucked up definition I know, but trust me that's all it takes to reason about type coercion, let's walk through some code examples.

console.log(Number('abc')) // NaN
console.log(Number({ x: 1, y: 2 })) // NaN
console.log(Number([1, 2, 3, 4])) // NaN
console.log(Number('5')) // 5
console.log(Number(true)) // 1

Sounds familiar code snippet? yeah that's the earlier one we used for exploring NaNs, that's type coercion, we're coercing the values from being string, objects, arrays to numbers

So my definition isn't that bad after all ^-^

But hey, that's not it, there's like hella lot more to coercion, so let's dig in.

valueOf

Ever tried numerifying your objects a.k.a calling console.log(Number({}))?

If you haven't, it will return NaN which is reasonable, we can't represent objects as numbers. but what if we desperately need to?

That's where valueOf comes into play, the valueOf operation can be used to override the internal behaviour of coercing objects to numbers, consider the following example.

Let's say you are hitting the github API to fetch some user repos and want to calculate total stars the user has.

let user = {
  username: 'ahmedosama-st',
  repos: [
    {
      id: 1,
      title: 'php-mvc-project',
      stars: 10,
    },
    {
      id: 2,
      title: 'filemarket',
      stars: 5,
    },
  ],
}

let userStars = Number(user) // returns NaN for sure.

so how can we encounter this situation to make the numeric representation of user object to be the sum of all it's repos' stars?

you can specifiy the desired behavior in the valueOf function in any object and it will be automatically invoked if you're trying to numerify the object.

let user = {
  username: 'ahmedosama-st',
  repos: [
    {
      id: 1,
      title: 'php-mvc-project',
      stars: 10,
    },
    {
      id: 2,
      title: 'filemarket',
      stars: 5,
    },
  ],

  valueOf() {
    return this.repos
      .map((repo) => repo.stars)
      .reduce((x, y) => x + y)
  },
}

console.log(Number(user)) // 15

Yaay, it works. but wtf are you doing xD?

Like seriously, why on earth would I want to represent some algorithm as the numerification of some object, imagine that you're a using some package that does so, how would you react to coercing your object returns you some intended behavior?

I'd strongly argue you should never do so, maybe that's lack of experience from my end that I haven't encountered a situation where this is useful, but until that happens, that's no use code.

Furthermore, speaking in terms of clean code and refactorability, it's highly likely you'll re-use such behavior which makes it more reasonable to extract it as an internal method that has a semantic name to deduce from it what it does.

let user = {
  username: 'ahmedosama-st',
  repos: [
    {
      id: 1,
      title: 'php-mvc-project',
      stars: 10,
    },
    {
      id: 2,
      title: 'filemarket',
      stars: 5,
    },
  ],

  totalRepoStars() {
    return this.repos
      .map((repo) => repo.stars)
      .reduce((x, y) => x + y)
  },
}

console.log(Number(user)) // NaN as it should be
console.log(user.totalRepoStars) // 15

It just took you one line of change, just change the method name and bam, you've achieved readability, clean code, yada yada yada.

toBoolean

This one is heavily used internaly, you might not have used it yourself, but the language makes a huge usage out of it, imagine everytime you're using an if statement or the double equal operator, the toBoolean operation is high likely to be called.

This operation is more of a look up operation than manipulating types, I guess you already know about the falsy table in JS.

Falsy values
zeros / 0, -0
empty string / "", ''
null
NaN
undefined
false

Any other value is considered a truthy value and passes in if statements.

So knowing that, what would you think will be the output of this code?

Boolean(new Boolean(false)) // ??

It would be true, because using the function Boolean means coerce the given input to a boolean type.

But using it as a constructor function, means constructing an ew object, which is of type object, which doesn't exist in the falsy table. thus it's true

toString

I think this is the most famous coercing operation, we use it daily debugging our objects and inspecting them, but the default behaviour isn't quite helpful as it produces [Object object] like wtf? I know it's an object yo.

So you can override this operation as well and specifiy your own implementation

let user = {
  username: 'ahmedosama_st',
  website: 'https://ahmedosama-st.github.io/codefolio/',
  skills: [
    {
      skill_name: 'php',
      years_of_experience: 5,
      favourite_frameworks: ['laravel', 'symfony', 'lumen'],
    },
    {
      skill_name: 'javascript',
      years_of_experience: 2,
      favourite_frameworks: [
        'react.js',
        'nest.js',
        'vue.js',
      ],
    },
  ],

  toString() {
    // return JSON.stringify(this, null, 2)
    // but I'd use util as it has coloring
    return require('util').inspect(this, false, null, true)
  },
}

Both implementations would work if you're having some nested object like the one I made as they're recursive operations.

And tbh, I think this override is a useful behaviour, atleast it makes sense and it's reasonable, you're having a deep inspection of your object which is logical.

ToPrimitive

It's some sort of internal operation that runs whenever you're trying to coerce some non-primitive type into a primtive (as the name suggests..)

And this operation can be though of the combination of the two previous operations, valueOf and toString

Let's see what ES specs has to say about this

"Aight, I'm not gonna read through that", don't worry I read it for you.

Let's see, the ToPrimitive operation is meant to coerce objects to other types, you can control how it behaves to even coerce to another non-primitive type, but c'mon, what are you thinking?

As you try to coerce some object to another type, to ToPrimtive operation is invoked, and the desired type is passed as an argument called hint, and the coercee -if that's a word- is passed as input

And the function goes through some steps as illustrated, 2 steps, If the coercee a.k.a input is an object, them some further steps will be done, otherwise return it untouched.

So you can say that it only works on coercing objects from a type to another, which is reasonable, what is the intent of coercing an object to an object?

So, what happens if I want to coerce my object to string or a number?

I think you can take a guess, if the passed hint is number the valueOf operation will be triggered first if existed, otherwise it'll return NaN, and by similarity, if you're coercing it into a string, if a custom implementation of toString is provided, it'll be invoked, otherwise, it'll fall back to the default behaviour and return [Object object]

I.E. you can replace the two previous operations with tis one and conditionally return desired output let's see this through code

let user = {
  username: 'ahmedosama-st',
  posts: [
    {
      id: 1,
      title: 'Exploring the V8 engine',
      url: 'example.com',
    },
  ],

  [Symbol.toPrimitive](hint) {
    if (hint == 'string') {
      return require('util').inspect(
        this,
        false,
        null,
        true,
      )
    } else if (hint == 'number') {
      return this.posts.length
    }
  },
}

console.log(String(user)) // {username: 'ahmedosama', posts: [...], ...}
console.log(Number(user)) // 1

Note: The default behaviour of ToPrimitive is to fallback to set hint to number if no hint is passed

Also note: If you try to do Boolean(user) it will not behave as you expect, it will fall back to the truthy/falsy table to check whether if this value if in the falsy table or not.

So what if you wish to alter this behavior? maybe for some reason you want to coerce some object to an array, Idk why would you do so, but let's see how it's possible.

The ToPrimitive is defined by a Symbol as a function, or a special function if you will, so you can invoke it this way and pass manual type as a hint and handle it within the function's body, but once more, please don't do so, it's complete nonsense.

let user = {
  username: 'ahmedosama-st',
  posts: [
    {
      id: 1,
      title: 'Exploring the V8 engine',
      url: 'example.com',
    },
  ],

  [Symbol.toPrimitive](hint) {
    if (hint == 'array') {
      return this.posts
    }
  },
}
// Note, that you have to call it yourself.
console.log(user[Symbol.toPrimitive]('array'))

Yes it will behave as you wish, and return the posts array, but once again, wtf are you thinking?

I think these coercions are better used to flex around or maybe in the context of an interview where you'd like to show that you deeply know types.

Meh, enough with these internal operations, let's move onto another topic.

Some corner cases of coercion

Sometimes coercing types can become a little bit messed up in JS, let's walk through this and reason about it.

Empty strings are zeros

Number('') // 0

This one is the root of all evil, I really can't get it why did JavaScript implement the coercion of an empty string to the zero number.

I'd like to think that the empty string is a representation of no characters, what on earth that has to do with zero?

Zero doesn't represent the absence of a numeric value, it's a real number and actually, it's one of the most important numbers in math.

So why to represent the absence of chars with a real numeric important value like zero when JavaScript actually has a solid existing type that's dedicated for non-numeric representations a.k.a NaN, like c'mon JS why are you doing this to us...

Even non-alpha a.k.a white spaces strings are represented as 0

Number('   \t\t\n') // 0

Because JavaScript gets in and says: "Ha got it, you're trying to turn a string into a number, let me trim it first :D", Yeah, thanks js..

But meh, what I'm arguing, I already stated it, if we're doing JS, we're gotta do it her way.

I think you're like rn, "Ahmed, you're a drama queen".

Well, that's a topic for another time, but trust me this idea of coercing empty strings causes all the non-reasonable behaviors of JS, and we'll get to them shortly.

Let's move onto the next weird coercion.

Null is also zero

Number(null) // 0

Yeey, empty strings aren't the only values that yield to zeros when coereced. Whispers: HATE YOU JS!

Well, once more, it should've been NaN, like come on! the ES spec itself stated that null represents the empty object or the absence of an object, zero isn't the right thing for that.

If you think this not a big deal, check out this code snippet from Stack over flow

And let's see the following example.

Number(null) // returns 0, sooo...
0 == null // sorry, false..
null == '' // again, false
null >= 0 // true FFS!!!
null >= '' // true, and here is where you quit JS
null <= '' // true, and now you're quite certain JS is fucked up

Hopefully you see how much this is messed up, and don't worry we'll walk through all these cases and why tf this behaviour occurs in the Equality section

Undefined is NaN

Number(undefined) // NaN

This one is the most hurting one, like really? that's when you realized you have a dedicated type to represent non-numeric representation, sigh, give me a break JS.

This is the most accurate one I'd say, becuase undefined is intended to represent absence of a value, and NaN as we discussed earlier, is the representation of non-valid numbers a.k.a absence of a numeric values.

Empty arrays are zeros

Number([]) // 0

I'll not whine or nag anymore I promise, this one is reasonable -According to JavaScript ofc-

What happens is this conversion is performed on two steps, because -as far as I know- JavaScript cannot convert non-primitives to numbers directly, it has to simplify it to the first possible type.

And unfortunately, the first primtive type that arrays yield to is string.. so yeah the loop repeats itself, and this operation goes as the following.

/* Intended */
Number([])

/* Goes through */

Number(String([]))
// or for simplification
let x = String([]) // ""
Number(x) // 0

So yeah, that's why I told ya, empty string coercions is the root of all evil.

Guess what, it even takes it further more to some more crazy stuff.

Number([null]) // 0
Number([undefined]) // 0

Amazing JS, why did you do that again? oh yeah because you decided that empty string is 0

So let's reason about what happens

Number([null])

We have agreed that this can't be done directly, but rather delegated to the coercion of strings.

Number(String([null]))

So what does String([null]) returns? Yup, the empty string.

Because JS decided that the stringification of an array is just removing the square brackets and non-occupied values, I.E, String([1,2,3]) returns "1,2,3" and if it happened that an array has either null or undefined it will be represented by empty spot like that String([1, 2, null, 4]) becomes "1,2,,4" the position of null is empty and that's completely nonsense, and what's more nonsensical, that String([null]) becomes empty string, like not even a comma floating around so it becomes NaN.

Conclusion

Type coercion exists whether you like it or not, so my hunch is, understand it, make use of what the language has to offer rather than dropping off some parts.

It's nonsensical to say that I will allow some type coercion of the langauge and ignore some other, it's whether you accept the whole feature or don't use it at all, and in this case, you can never ignore coercion, the language uses it internaly.

What I would recommend is, make sure that you avoid the corner cases of coercion, make sure that you're accepting just the right types for your functions.

If you're working on some algorithm, some sorting algorithm that deals with users and sorts them based on some criteria, if you want to make this algorithm flexible in some sorts, I'd say I may accept either strings for usernames maybe or numbers for reputation or total stars if they're github users , and that would be it, I'll make type guards for any other type to reject it.

function sort(a, b) {
  if (!isAcceptableType(a) || !isAcceptableType(b)) return

  // Perform my algorithm if they're acceptable.

  function isAcceptableType(value) {
    return ['string', 'number'].includes(typeof value)
  }
}

Yes I've reduced the hassle of dealing with objects, booleans, arrays and also allowed some sort of type coercion, sure I'd go further more handling some of the weird edge cases like coercing the empty string to zero and other cases we've looked at, but I will end up with some flexible sorting algorithm

I think this sums it up for this article, sure I haven't covered all corner cases or coercion or all the processes of coercion, because it would be more like 2 hours of reading and too much effort to do.

So I'll leave you some resources for further reading.

Further inspection

Cheerio, have a nice drink and a wish you a very pleasing day 💜

Consider supporting/following me

30