Typescript/Javascript Internals: Everything (okay, almost everything) is an Object


I’m on a daring journey to peel back as many layers as possible. Stare into the abyss for 4 years: become something akin to Hugh Stormward of the Mage Errant series (by John Bierce) or end up with another lofty goal that goes nowhere. This year, this particular year is the year I believe I now have the fundamentals locked in enough for me to go down the stack.
Of course, every journey towards the soulless metal starts with an understanding of C. What I do is read and then see how a concept is implemented in languages like TS/JS, Golang and Python. So far, nothing has rocked my world like arrays did.
Modern C (by John Gustedt) says arrays can’t be compared.This is fine. Even in the sugar filled world of TS/JS, you can’t do array1 (1,2,3) === array2 (1,2,3)
. It’ll always return false since arrays are reference types and reference A ≠ reference B. But then the book says arrays can’t be assigned to. You are absolutely not allowed to do this:
int a[3] = {1,2,3};
int b[3];
b = a; // Compilation error
This is because arrays in C are blocks of memory, not assignable values and you cannot reassign that name to another memory block. Unlike TS/JS where arrays are assignable because they are simply object references. Const arr = [1,2,3]
Doing this creates an instance of the Array object and arr stores a reference to the object and not the data itself. Therefore, this is a valid operation:
const arr = [1,2,3];
const b = arr; // Works, it's just a reference copy
b[0] = 99;
console.log(arr); // [99,2,3] — same object
When you do arr[1]
, the V8 engine simply looks up the property “1”
on the object. Oh!! object? not an array?
const arr = [1,2,3];
const b = arr;
b[0] = 99;
console.log(arr); // [99, 2, 3]
console.log(Object.keys(arr)); // ["0", "1", "2"]
console.log(Object.values(arr)); // [99, 2, 3]
In the TS/JS world, everything (okay, almost everything) is an object. Functions, classes, arrays, regular expressions, dates, promises, maps, and even many primitives through autoboxing are all objects with their own properties and methods, simply dressed in different syntactic clothing.
// Functions as objects
function greet() { return "Hello"; }
(greet as any).customProperty = "I'm an object property";
console.log((greet as any).customProperty); // "I'm an object property"
console.log(greet.hasOwnProperty("customProperty")); // true
console.log(Object.keys(greet)); // ["customProperty"]
console.log(greet instanceof Object); // true
// Classes as objects
class Person {}
(Person as any).staticMethod = function () { return "Static!"; };
console.log((Person as any).staticMethod()); // "Static!"
// Arrays as objects
const arr: number[] = [1, 2, 3];
(arr as any).customProp = "I'm not an index";
console.log((arr as any).customProp); // "I'm not an index"
console.log(typeof arr); // "object"
console.log(arr instanceof Object); // true
// RegExp as objects
const pattern: RegExp = /test/;
(pattern as any).customProp = "Custom property on RegExp";
console.log((pattern as any).customProp); // "Custom property on RegExp"
// Dates as objects
const today: Date = new Date();
(today as any).description = "Today's date";
console.log((today as any).description); // "Today's date"
// Promises as objects
const promise: Promise<number> = Promise.resolve(42);
(promise as any).extraInfo = "Pending operation";
console.log((promise as any).extraInfo); // "Pending operation"
// Maps as objects
const map = new Map();
(map as any).description = "User data";
console.log((map as any).description); // "User data"
// Primitives get temporarily boxed
const str: string = "hello";
console.log((str as any).__proto__ === String.prototype); // true
console.log((str as any).constructor === String); // true
Lool, of course this opens up a world of evil, so much evil:
But behind the chaos lies a crucial lesson: when everything (okay, almost everything) is an object, nothing is sacred. Functions can lie, arrays can spy, and objects can carry payloads buried in their prototype chains. If you are building critical software with TypeScript, always assume that as any
is a loaded weapon pointed at your runtime. While autoboxing and shared prototype chains make TS/JS powerful, they can also expose a system to significant risks. To demonstrate (consider this horrible piece of code which is not likely to make it to a production server), merging an unsanitized user input can pollute Object.prototype
and thereby make inherited objects to behave in an unintended way.
function merge(target: any, source: any): void {
for (const key in source) {
if (typeof source[key] === "object" && source[key] !== null) {
if (!target[key]) {
target[key] = {};
}
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
const payload = JSON.parse(`{
"__proto__": {
"isAdmin": true
}
}`);
let innocent = {"username": "I am innocent"};
merge(innocent, payload);
const victim = {"username": "victim"};
console.log((victim as any).isAdmin); // true
console.log((innocent as any).isAdmin); // true
Apart from security, prototype chaining also affects speed. When the V8 engine tries to access an object property, it first checks if the property exists on the object. If it does not exist, it checks up the prototype chain, moving from one prototype to the next, until it either finds the property or reaches the end of the chain (typically Object.prototype
). To optimize this chaos, the V8 engine uses a combination of techniques such as inline caching, hidden classes, escape analysis and heap optimizations.
For instance, after a successful property lookup, the V8 engine caches the location. On next lookup of the same property, the engine skips looking through the parent chains and simply goes to the cached location. V8 combines hidden classes with inline caching for maximum efficiency. Classes are assigned based on the shape of an object (i.e the set of properties of an object). Objects that share the same shape will have the same hidden class, enabling faster access to properties by directly indexing into the object’s structure. When properties are added or removed from an object, its hidden class may change. This helps to avoid unnecessary prototype chain traversal.
Pseudo-machine code 1: load hidden_class of obj
Pseudo-machine code 2: compare with known_hidden_class (HC1)
Pseudo-machine code 3: if equal, go to offset and return (cached property location in object)
Pseudo-machine code 4: else, fallback to full property lookup (check prototype chain)
Your code should help the engine and not work against it by:
Maintaining consistent object shapes:
// Good: Same shape for all objects interface User { id: number; name: string; } const user1: User = { id: 1, name: "David" }; const user2: User = { id: 2, name: "Bolaji" }; // Bad: Different shapes or property order const badUser1 = { id: 1, name: "David" }; const badUser2 = { name: "Bolaji", id: 2 }; // different order = different shape
Initializing all properties upfront
// Good: All properties initialized at creation interface Product { id: number; name: string; price: number; } const product: Product = { id: 101, name: "Widget", price: 9.99 }; // Bad: Properties added over time interface DynamicProduct { id: number; name?: string; price?: number; } const badProduct: DynamicProduct = { id: 101 }; badProduct.name = "Widget"; // Each addition creates a new hidden class badProduct.price = 9.99; // Another new hidden class
Keeping functions monomorphic
// Good: Function always receives same object shape function getName(user: User): string { return user.name; // V8 can optimize this property access } // Bad: Polymorphic function type UserOrProduct = { id: number; name: string } | { productId: number; title: string }; function getIdentifier(obj: UserOrProduct): number { // Sometimes gets passed a user object // Sometimes gets passed a product object return "id" in obj ? obj.id : obj.productId; }
Avoiding sparse arrays
// Good: Dense array, same types const scores: number[] = [75, 80, 95, 62, 88]; // Bad: Sparse array const sparseScores: number[] = []; sparseScores[0] = 75; sparseScores[10] = 80; // Creates holes
Conclusion
The bottom line: understanding the fundamental aspect of TS/JS is crucial. The language’s flexibility isn’t always your friend, and what seems like a simple operation can quickly turn into an unforeseen problem. But no worries though, chaos is kind of fun.
Subscribe to my newsletter
Read articles from David Oluwatobi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

David Oluwatobi
David Oluwatobi
A software engineer struggling to keep up with his writing schedule