Skip to main content
Navigation
HomeTechnical ReferenceJournalGitHubGitHub
Sidebar — toggle document categories via the logo
Categories

Compilation & JS Prototype Model

Overview

TypeScript is a superset of JavaScript that adds static type checking at compile time. At runtime, all types are erased — the executing code is pure JavaScript. This document covers how TypeScript compiles to JavaScript, the JavaScript prototype-based inheritance model, how it differs from Python's class-based model, and the practical implications for developers working across both languages.

How TypeScript compiles to JavaScript

TypeScript compilation is type erasure plus downleveling. The compiler:

  1. Parses .ts/.tsx into an AST.
  2. Type-checks the AST using the type system — this is where errors are emitted.
  3. Strips type annotations — interfaces, type aliases, generics, parameter types, return types are all removed.
  4. Downlevels modern ECMAScript syntax to the configured target (e.g., async/await → generators for ES2015 target).
  5. Emits .js (and optionally .d.ts and .js.map) files.

Type erasure example

// source.ts
interface User {
id: number;
name: string;
}

function greet(user: User): string {
return `Hello, ${user.name}`;
}

const alice: User = { id: 1, name: "Alice" };
greet(alice);

Compiles to (target: ES2020, stripped of comments):

// output.js
function greet(user) {
return `Hello, ${user.name}`;
}

const alice = { id: 1, name: "Alice" };
greet(alice);

No interfaces, no type annotations, no : User, no : string return type. The runtime behaviour is identical — TypeScript adds zero runtime overhead.

Enums — the exception

Unlike most TypeScript features, enum generates runtime code:

enum Color { Red, Green, Blue }

Compiles to:

var Color;
(function (Color) {
Color[Color["Red"] = 0] = "Red";
Color[Color["Green"] = 1] = "Green";
Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));

For zero-runtime-overhead enums, use const enum (inlines values at compile time) or string literal unions (type Color = "red" | "green" | "blue").

Downleveling

When target is lower than the syntax used:

// source.ts
class Foo {
#privateField = 42;
get value() { return this.#privateField; }
}

With target: ES5:

var Foo = (function () {
function Foo() {
// ... polyfilled private field via WeakMap
this._privateField = 42;
}
Object.defineProperty(Foo.prototype, "value", {
get: function () { return this._privateField; },
enumerable: false, configurable: true
});
return Foo;
})();

Declaration files (.d.ts)

.d.ts files describe the shape of JavaScript libraries without generating any output:

// lodash.d.ts
declare module "lodash" {
export function chunk<T>(array: T[], size: number): T[][];
export function debounce<T extends (...args: any[]) => any>(
fn: T, wait: number, options?: { leading?: boolean }
): T;
}

These are the bridge between typed and untyped worlds — they provide type information for the compiler but produce zero bytes of JavaScript.

tsconfig key options

OptionEffect
targetECMAScript version for output (ES2022, ESNext, etc.)
moduleModule system in output (ESNext, NodeNext, CommonJS)
moduleResolutionHow imports are resolved (NodeNext for modern Node, bundler for Vite/Next.js)
strictEnables all strict type-checking flags
noEmitType-check only — don't produce output files
declarationGenerate .d.ts files alongside .js

The JavaScript prototype model

JavaScript uses prototypal inheritance rather than classical class-based inheritance. Every object has an internal [[Prototype]] link to another object. Property lookup traverses this chain.

__proto__ vs prototype

These are often confused but are distinct:

PropertyOnPurpose
__proto__ (or [[Prototype]])Every objectPoints to the object's prototype — used for property lookup
prototypeOnly functionsThe object that will be assigned as [[Prototype]] of instances created with new
function Dog(name) {
this.name = name;
}

Dog.prototype.bark = function() {
return `${this.name} says woof`;
};

const fido = new Dog("Fido");

console.log(fido.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.constructor === Dog); // true
console.log(fido.bark()); // "Fido says woof"

The relationship:

fido.__proto__ ─────────► Dog.prototype

├── bark: function
├── constructor: Dog

__proto__ │

Object.prototype

├── toString: function
├── hasOwnProperty: function

__proto__ │

null

Property lookup (the prototype chain)

When you access fido.bark:

  1. Look at fido (the instance) — "bark" not found.
  2. Follow fido.__proto__ to Dog.prototype — "bark" found. Call it with fido as this.

When you access fido.toString:

  1. fido → not found.
  2. Dog.prototype → not found.
  3. Object.prototype → found.

This is the prototype chain — a linked list of objects. Lookup stops at null (the prototype of Object.prototype).

Constructor functions and new

The new keyword does four things:

function Person(name) {
// 1. A new empty object is created: {}
// 2. The object's [[Prototype]] is set to Person.prototype
// 3. The constructor runs with `this` = the new object:
this.name = name;
// 4. The new object is returned (unless the constructor returns an object)
}

const p = new Person("Alice");
// Equivalent to:
// const p = Object.create(Person.prototype);
// Person.call(p, "Alice");

ES6 class syntax is syntactic sugar

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

speak() {
return `${this.name} makes a noise`;
}
}

class Dog extends Animal {
speak() {
return `${this.name} barks`;
}
}

Is functionally equivalent to:

function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise`;
};

function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
return `${this.name} barks`;
};

The class syntax is clearer, but the underlying mechanism is identical — it's all prototype chains.

Object.create() — the purest prototypal pattern

const animal = {
speak() { return `${this.name} makes a noise`; }
};

const dog = Object.create(animal); // dog.__proto__ === animal
dog.name = "Fido";
console.log(dog.speak()); // "Fido makes a noise"

No constructors, no new — just objects linked to other objects. This is the pattern that most directly expresses JavaScript's prototypal nature.

JavaScript inheritance vs Python inheritance

Key differences

ConceptJavaScriptPython
Inheritance modelPrototypal (delegation)Class-based (instance-of)
Base mechanismObject → prototype chain up to nullClass → MRO linearization (C3)
Multiple inheritanceNot supported natively (mixins via Object.assign)Fully supported with C3 linearization
Method lookupWalk __proto__ chain until found or nullWalk __mro__ tuple until found or AttributeError
this / selfDynamically bound (depends on call site)Static (explicit first parameter)
Class creationclass is sugar over constructor + prototypetype(name, bases, namespace) via metaclass
MetaprogrammingProxy, Reflect, monkey-patchingMetaclasses, descriptors, __getattr__
Privacy#privateField (hard private), _convention_convention only — no hard privacy
Static methodsstatic keyword (on constructor)@staticmethod (plain function), @classmethod (receives class)

Inherited vs own properties

JavaScript distinguishes between own and inherited properties:

const obj = { a: 1 };

console.log(obj.hasOwnProperty("a")); // true (own)
console.log(obj.hasOwnProperty("toString")); // false (inherited from Object.prototype)
console.log("toString" in obj); // true (in traverses the chain)

for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// only own enumerable properties
}
}

Python has no direct equivalent — obj.__dict__ only contains instance attributes, and dir(obj) shows both instance and class attributes combined.

Method binding — the critical difference

In JavaScript, this is determined at call time, not definition time:

const obj = {
name: "Alice",
greet() { return `Hello, ${this.name}`; }
};

const fn = obj.greet;
fn(); // "Hello, undefined" — this is global/window, not obj

// Fix with .bind()
const bound = obj.greet.bind(obj);
bound(); // "Hello, Alice"

// Arrow functions capture lexical this
const obj2 = {
name: "Bob",
greet: () => `Hello, ${this.name}` // `this` from enclosing scope
};

In Python, self is always the instance the method was accessed from (bound at access time via the descriptor protocol):

class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"

p = Person("Alice")
fn = p.greet
fn() # "Hello, Alice" — self is bound to p

This is one of the most common footguns when moving between Python and JavaScript.

Prototype pollution (JS-specific danger)

Because prototypes are mutable objects, JavaScript is vulnerable to prototype pollution:

// Dangerous: modifies all objects
Object.prototype.polluted = true;

const x = {};
console.log(x.polluted); // true — every object now has this property

Python's class objects are also technically mutable, but instance __dict__ lookups take priority over class attributes (for non-data descriptors), making the surface area smaller. More importantly, built-in types in Python are immutable at the C level — you cannot add methods to int or str.

Type-system contrast

TypeScript adds a structural type system on top of JavaScript, erased at runtime:

interface Named {
name: string;
}

function greet(entity: Named) {
console.log(`Hello, ${entity.name}`);
}

// Works — structural typing checks shape, not declaration
greet({ name: "Alice", age: 30 });

Python uses a nominal type system (types must be explicitly related via inheritance or protocols):

class Named(Protocol):
name: str

def greet(entity: Named) -> None:
print(f"Hello, {entity.name}")

@runtime_checkable # requires isinstance support
class Person(Named):
name: str

TypeScript's structural typing is a better fit for JavaScript's duck-typing philosophy. Python's Protocol brings similar structural typing, but with opt-in runtime checkability.

Practical cross-language patterns

Polymorphism without inheritance

JavaScript (duck typing):

function makeSound(animal) {
console.log(animal.speak());
}

makeSound({ speak: () => "meow" }); // no inheritance needed

Python (duck typing / Protocol):

def make_sound(animal):
print(animal.speak())

# Works with any object that has .speak()
make_sound(type("Cat", (), {"speak": lambda self: "meow"})())

Both languages support ad-hoc polymorphism through duck typing, but TypeScript's structural typing makes it safer at compile time.

Mixins

JavaScript:

const Serializable = Base => class extends Base {
toJSON() { return JSON.stringify(this); }
};

class User extends Serializable(class {}) {
name = "Alice";
}

Python:

class SerializableMixin:
def to_json(self):
return json.dumps(self.__dict__)

class User(SerializableMixin):
name = "Alice"

Both achieve composition over inheritance through mixins, but Python's MRO provides deterministic method resolution, while JavaScript's prototype chain is a strictly linear delegation.

See also