Extreme JavaScript Type Coercion
Kyle Schneider
Posted on February 14, 2023
Introduction
Last week I stumbled upon this silly bit of JavaScript:
console.log(('b' + 'a' + + 'a' + 'a').toLowerCase());
// RETURNS: banana
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
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
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
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
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)
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
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
Programmatically:
const zero = '+[]';
const one = '+!![]';
function getNum(n) {
if (n == 0) return zero;
return Array(n).fill(one).join(' + ');
};
console.log(getNum(7));
// RETURNS: +!![] + +!![] + +!![] + +!![] + +!![] + +!![] + +!![]
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
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]
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 stringNaN
-
{}+[]
yields the string[object Object]
-
!![]+[]
yields the stringtrue
-
![]+[]
yields the stringfalse
-
(+!![]/+[])+[]
yields the stringInfinity
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
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)}]`;
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] }
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(' + ');
}
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)}]`;
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)})`;
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)}]`;
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
So capital C in our code is
chars.C = `(()=>{})[${convertString('constructor')}](${convertString('return escape')})()(${chars['\\']})[${getNum(2)}]`
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(' + ');
}
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!");'));
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
Posted on February 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.