Extreme JavaScript Type Coercion

kschneider0

Kyle Schneider

Posted on February 14, 2023

Extreme JavaScript Type Coercion

Introduction

Last week I stumbled upon this silly bit of JavaScript:

console.log(('b' + 'a' + + 'a' + 'a').toLowerCase());
// RETURNS: banana
Enter fullscreen mode Exit fullscreen mode

I had seen similar strange code snippets before, but had not felt the urge to explore the mechanics of what was happening until I came across a wonderful YouTube video by Low Byte Productions [3]. In the video titled "JavaScript Is Weird EXTREME EDITION", we are shown a short script which converts JavaScript into JavaScript composed exclusively from the characters ({[/>+!-=]}). The new and comically unreadable code would of course compile exactly the same as the original. This post recaps a weekend spent looking into JavaScript type coercion and reproducing Low Byte Production's extremely strange JavaScript compiler.

Type Coercion

Definition

In some programming languages, the combination of different data types is not allowed. For example, in Python:

print(1 + "1")
# RETURNS: TypeError: can only concatenate str (not "int") to str
Enter fullscreen mode Exit fullscreen mode

JavaScript is more friendly than Python in that it will make an attempt to combine different data types. Type coercion is the automatic or implicit conversion of values from one data type to another [1]. In JavaScript, the same code yields a result:

console.log(1 + "1");
// RETURNS: 11

console.log(1 + 1 + "1");
// RETURNS: 21

console.log("1" + 1 + 1);
// RETURNS: 111
Enter fullscreen mode Exit fullscreen mode

In the second example, JavaScript adds the first two 1's to 2 since they are both of type Number, and then converts this to a string before adding the final "1" of type String.

This highlights the importance of the difference between == and === in JavaScript. The == operator uses type coercion, whereas === only matches equal types.

console.log(1 == "1");
// RETURNS: true

console.log(1 === "1");
// RETURNS: false
Enter fullscreen mode Exit fullscreen mode

The Unary Plus Operator

The unary plus operator + precedes its operand and will attempt to convert the operand into a number before returning the operand. It can convert strings that represent integers or floats to numbers, and will also convert the values true, false, and null [2].

const x = 1;
const y = -1;

console.log(+x);
// RETURNS: 1

console.log(+y);
// RETURNS: -1

console.log(+true);
// RETURNS: 1

console.log(+false);
// RETURNS: 0

console.log(+'');
// RETURNS: 0

console.log(+'2');
// RETURNS: 2

console.log(+'3.1415');
// RETURNS: 3.1415

console.log(+'hello');
// RETURNS: NaN
Enter fullscreen mode Exit fullscreen mode

Strange Type Coercion

Here are some other examples of type coercion that may be less intuitive:

console.log(+[]);
// RETURNS: 0

console.log(![]);
// RETURNS: false

console.log(!![]);
// RETURNS: true

console.log(+{});
// RETURNS: NaN

console.log(+{}+[]);
// RETURNS: 'NaN'

console.log([]+[]);
// RETURNS: '' (empty string)
Enter fullscreen mode Exit fullscreen mode

Notice that adding +[] to an expression will convert it directly to a string.

Since the unary plus of a non-numeric string returns NaN, we can write some funny JavaScript that use type coercion. What will happen when we try to evaluate 'ba' + (+'a')? This becomes 'ba' + NaN since 'a' is non-numeric. Now since JavaScript is adding a type number to a type string (yes, typeof NaN === number), it will convert the NaN to a string and return 'baNaN'. Now we can see why the first code snippet logs banana. The value of the string with the unary operator does not need to be a, it just needs to be non-numeric:

console.log(('b' + 'a' + + 'pomegranate' + 'a').toLowerCase());
// RETURNS: banana
Enter fullscreen mode Exit fullscreen mode

Numbers and Letters With Just ([!+])

Since +true evaluates to 1 and !![] evaluates to true, we can create the positive integers:

console.log(+!![]);
// RETURNS: 1

console.log(+!![] + +!![]);
// RETURNS: 2

console.log(+!![] + +!![] + +!![]);
// RETURNS: 3
Enter fullscreen mode Exit fullscreen mode

Programmatically:

const zero = '+[]';
const one = '+!![]';

function getNum(n) {
    if (n == 0) return zero;
    return Array(n).fill(one).join(' + ');
};

console.log(getNum(7));
// RETURNS: +!![] + +!![] + +!![] + +!![] + +!![] + +!![] + +!![]
Enter fullscreen mode Exit fullscreen mode

This repeated string evaluates to 7 in JavaScript. Try it in the console! Now that we have all the numbers, let's use them to obtain some letters.

Recall that JavaScript's type coercion gives !![]+[] === true, where true is a string because of the +[]. We can access the letters of true now with string indexing:

console.log((!![]+[])[+[]]);
// RETURNS: t
console.log((!![]+[])[+!![]]);
// RETURNS: r
console.log((!![]+[])[+!![] + +!![]]);
// RETURNS: u
console.log((!![]+[])[+!![] + +!![] + +!![]]);
// RETURNS: e
Enter fullscreen mode Exit fullscreen mode

Note that we could have used the getNum function earlier, e.g. use getNum(3) in place of +!![] + +!![] + +!![] for the last letter, but here we can see that the characters are truly derived from just the set ([!+]).

Collecting Characters

Eventually, we would like to write any character in terms of our symbol set. The final method to achieve this will require us to have access to constructor, toString, return, escape, charCodeAt, \, and the space character. This means we need to obtain the following characters using only ({[/>+!-=]}):

fromChadetSingcup[space][backslash] 
Enter fullscreen mode Exit fullscreen mode

Easy Words

The first round of characters will be just like the example above using the following strings derived from JavaScript type coercion:

  • +{}+[] yields the string NaN
  • {}+[] yields the string [object Object]
  • !![]+[] yields the string true
  • ![]+[] yields the string false
  • (+!![]/+[])+[] yields the string Infinity

Here are the characters from Infinity using string interpolation with our getNum function:

// +!![]/+[] => Infinity
console.log(`((+!![]/+[])+[])[${getNum(3)}]`);
// RETURNS: i
console.log(`((+!![]/+[])+[])[${getNum(4)}]`);
// RETURNS: n
Enter fullscreen mode Exit fullscreen mode

We will store the string value for all of these characters using the chars object:

const zero = '+[]';
const one = '+!![]';

function getNum(n) {
    if (n == 0) return zero;
    return Array(n).fill(one).join(' + ');
};

// WANT: fromChadetSingcup[space][backslash]

const chars = {};

// +{} => NaN
chars.a = `(+{}+[])[${getNum(1)}]`;

// {}+[] => '[object Object]'
chars.b = `({}+[])[${getNum(2)}]`;
chars.o = `({}+[])[${getNum(1)}]`;
chars.e = `({}+[])[${getNum(4)}]`;
chars.c = `({}+[])[${getNum(5)}]`;
chars.t = `({}+[])[${getNum(6)}]`;
chars[' '] = `({}+[])[${getNum(7)}]`;

// ![] => false
chars.f = `(![]+[])[${getNum(0)}]`;
chars.s = `(![]+[])[${getNum(3)}]`;

// !![] => true
chars.r = `(!![]+[])[${getNum(1)}]`;
chars.u = `(!![]+[])[${getNum(2)}]`;

// +!![]/+[] => Infinity
chars.i = `((+!![]/+[])+[])[${getNum(3)}]`;
chars.n = `((+!![]/+[])+[])[${getNum(4)}]`;
Enter fullscreen mode Exit fullscreen mode

Complex Words

Constructors

Calling the constructor method on a JavaScript variable will return the constructor of that type. If we attach our friend +[], to it, we get the constructor name as a string:

console.log((1)['constructor']+[]);
// RETURNS: function Number() { [native code] }
Enter fullscreen mode Exit fullscreen mode

The next set of characters will get a string by accessing this constructor method. We have all of the characters necessary to make the word constructor, so define the function convertString which converts the input string to our symbol set. Note that this will only work for strings whose characters are already defined in chars.

function convertString(string) {
    return string.split('').map(char => chars[char]).join(' + ');
}
Enter fullscreen mode Exit fullscreen mode

We get three letters from constructors, two from strings and one from regexs:

// ([]+[])['constructor']+[] => 'function String() { [native code] }'
chars.S = `([]+([]+[])[${convertString('constructor')}])[${getNum(9)}]`;
chars.g = `([]+([]+[])[${convertString('constructor')}])[${getNum(14)}]`;

// (/-/)['constructor']+[] => 'function RegExp() { [native code] }
chars.p = `([]+(/-/)[${convertString('constructor')}])[${getNum(14)}]`;
Enter fullscreen mode Exit fullscreen mode

toString(n)

The next three letters use a clever trick with the toString method of numbers. The method takes a target base as parameter and will convert the integer it is called on to that base as a string.

  • 13 in base 14 is d
  • 17 in base 18 is h
  • 22 in base 23 is m
// convert base 10 ints to hexadecimal to get letters
chars.d = `(${getNum(13)})[${convertString('toString')}](${getNum(14)})`;
chars.h = `(${getNum(17)})[${convertString('toString')}](${getNum(18)})`;
chars.m = `(${getNum(22)})[${convertString('toString')}](${getNum(23)})`;
Enter fullscreen mode Exit fullscreen mode

Capital C

The last letter, C, is the most complex. Before we can get C, we need the backslash character. We need to store \\ in chars, since, when evaluated, the first backslash escapes. This is achieved by converting a stringified regex with four backslashes to a string and accessing index 1:

// regex
chars['\\'] = `(/\\\\/+[])[${getNum(1)}]`;
Enter fullscreen mode Exit fullscreen mode

The JavaScript escape function will replace all characters with ASCII escape sequences [4]. To get C, we will use the fact that escape('\\') === '%5C'. We can get the escape function using the function constructor like so:

console.log((()=>{})['constructor']('return escape')());
// RETURNS: [Function: escape]
console.log((()=>{})['constructor']('return escape')()('\\'));
// RETURNS: %5C
Enter fullscreen mode Exit fullscreen mode

So capital C in our code is

chars.C = `(()=>{})[${convertString('constructor')}](${convertString('return escape')})()(${chars['\\']})[${getNum(2)}]`
Enter fullscreen mode Exit fullscreen mode

Write ANY Character With Just ({[/>+!-=]})

Now we are equipped to produce any other character with our strange syntax. We will edit the convertString function to handle inputs that are not already in the map. If the input character is not in chars, get the character code of the single value string with charCodeAt(0). Then return the string constructor called with the fromCharCode(charCode) method.

function convertString(string) {
    return string.split('').map(char => {
        if (!(char in chars)) {
            const charCode = char.charCodeAt(0);
            return `([]+[])[${convertString('constructor')}][${convertString('fromCharCode')}](${getNum(charCode)})`;
        }
        return chars[char];
    }).join(' + ');
}
Enter fullscreen mode Exit fullscreen mode

Now we can convert any string into a string containing only ({[/>+!-=]}). The last code we'll add at the ability to execute our 'symbol code'. Here is the result:

const zero = '+[]';
const one = '+!![]';

function getNum(n) {
    if (n == 0) return zero;
    return Array(n).fill(one).join(' + ');
};

const chars= {};

function convertString(string) {
    return string.split('').map(char => {
        if (!(char in chars)) {
            const charCode = char.charCodeAt(0);
            return `([]+[])[${convertString('constructor')}][${convertString('fromCharCode')}](${getNum(charCode)})`;
        }
        return chars[char];
    }).join(' + ');
}

// +{} => NaN
chars.a = `(+{}+[])[${getNum(1)}]`;

// {}+[] => '[object Object]'
chars.b = `({}+[])[${getNum(2)}]`;
chars.o = `({}+[])[${getNum(1)}]`;
chars.e = `({}+[])[${getNum(4)}]`;
chars.c = `({}+[])[${getNum(5)}]`;
chars.t = `({}+[])[${getNum(6)}]`;
chars[' '] = `({}+[])[${getNum(7)}]`;

// ![] => false
chars.f = `(![]+[])[${getNum(0)}]`;
chars.s = `(![]+[])[${getNum(3)}]`;

// !![] => true
chars.r = `(!![]+[])[${getNum(1)}]`;
chars.u = `(!![]+[])[${getNum(2)}]`;

// +!![]/+[] => Infinity
chars.i = `((+!![]/+[])+[])[${getNum(3)}]`;
chars.n = `((+!![]/+[])+[])[${getNum(4)}]`;

// ([]+[])['constructor']+[] => 'function String() { [native code] }'
chars.S = `([]+([]+[])[${convertString('constructor')}])[${getNum(9)}]`;
chars.g = `([]+([]+[])[${convertString('constructor')}])[${getNum(14)}]`;

// (/-/)['constructor']+[] => 'function RegExp() { [native code] }
chars.p = `([]+(/-/)[${convertString('constructor')}])[${getNum(14)}]`;

// regex
chars['\\'] = `(/\\\\/+[])[${getNum(1)}]`;

// convert base 10 ints to hexadecimal to get letters
chars.d = `(${getNum(13)})[${convertString('toString')}](${getNum(14)})`;
chars.h = `(${getNum(17)})[${convertString('toString')}](${getNum(18)})`;
chars.m = `(${getNum(22)})[${convertString('toString')}](${getNum(23)})`;

// escape function
chars.C = `(()=>{})[${convertString('constructor')}](${convertString('return escape')})()(${chars['\\']})[${getNum(2)}]`

const compile = code => `(()=>{})[${convertString('constructor')}](${convertString(code)})()`;

console.log(compile('console.log("Hello, World!");'));
Enter fullscreen mode Exit fullscreen mode

Whatever JavaScript code is passed as a string on the last line will be logged to the console as valid JavaScript using only ({[/>+!-=]})!

I encourage you to paste the above code in your dev tools and see the output for yourself. Copy that output and run it as JavaScript to see Hello, World! printed to the console.

To reiterate, I did not come up with this code. I followed the video by Low Byte Productions [3] and read MDN documentation [1-2, 4] to understand the mechanics of the code and be able to reproduce and explain the code.

References:

[1] MDN - Type Coercion - https://developer.mozilla.org/en-US/docs/Glossary/Type_coercion
[2] MDN - Unary Plus - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Unary_plus
[3] Low Byte Productions - JavaScript Is Weird (EXTREME EDITION) - https://www.youtube.com/watch?v=sRWE5tnaxlI&t=50s

[4] MDN - escape() - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/escape

💖 💪 🙅 🚩
kschneider0
Kyle Schneider

Posted on February 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Extreme JavaScript Type Coercion
javascript Extreme JavaScript Type Coercion

February 14, 2023