#Classes in TypeScript
A TypeScript class is a JavaScript class with types written on it. Everything that runs — the constructor, methods, fields, extends, super, #private, static — is plain JavaScript and behaves exactly as it does there. TypeScript adds two kinds of things on top: checks (every field must be declared and initialised, method calls are type-checked, overrides must be compatible) and a handful of TypeScript-only keywords (private, protected, public, readonly, abstract, override, implements, parameter properties). Most of those keywords are erased; a couple (parameter properties) generate code. The topics below go through each one and show what's left at runtime.
The one idea that makes classes "interesting" in TypeScript, as my notes put it: a class declaration creates two things with one name. class Point {} creates a value — the constructor function you call with new — and a type — the shape of the instances it creates. When you write Point after a colon, you mean the instance type; when you write new Point() or typeof Point, you mean the constructor.
And because TypeScript is structurally typed (see What TypeScript is (and isn't)), that instance type is just a shape. Any object with the same members is accepted where a Point is expected, whether or not it came from the class.
class Person {
name: string;
age = 0; // type inferred from the initialiser: number
nickname?: string; // optional: string | undefined, no initialiser needed
constructor(name: string) {
this.name = name;
}
greet(): string {
return `Hi, I'm ${this.name}`;
}
}
class Broken {
title: string; // ❌ Property 'title' has no initializer and is not definitely assigned in the constructor.
setUp() {
this.label = "x"; // ❌ Property 'label' does not exist on type 'Broken'.
}
}- In JavaScript you can create a property anywhere by assigning
this.x = …. TypeScript requires every instance property to be declared in the class body, so it knows the instance shape up front.name: stringis a declaration; the constructor then assigns it. age = 0declares and initialises in one go; the typenumberis inferred from0.nickname?: stringis optional, so its type isstring | undefinedand it's allowed to stay unset.- In
Broken,title: stringis declared but never assigned. Understrict(strictPropertyInitialization) the checker looks at the constructor, sees no assignment on any path, and reports TS2564 "Property 'title' has no initializer and is not definitely assigned in the constructor." Otherwisenew Broken().title.toUpperCase()would type-check and crash. this.label = "x"fails with TS2339 "Property 'label' does not exist on type 'Broken'." — assigning doesn't declare. This catches typos likethis.lablethat JavaScript would silently turn into a new property.- If a property really is set somewhere the checker can't see (by a framework, or an
init()method you always call), you can writetitle!: string— the definite assignment assertion. It switches off the check for that property, so use it sparingly.
The class name as a type, and as a value:
class Point {
x = 0;
y = 0;
}
const p: Point = { x: 1, y: 2 }; // fine: it has the right shape
console.log(p instanceof Point); // false — it never went through the constructor
function create(ctor: typeof Point): Point {
return new ctor();
}
const q = create(Point); // Point { x: 0, y: 0 }const p: PointusesPointas a type: "an object withx: numberandy: number". The object literal has exactly that shape, so it's accepted — the checker compares structure, not where the object came from.- At runtime,
p instanceof Pointisfalse.instanceofchecks the prototype chain, and a literal's prototype isObject.prototype. This is the classic structural-typing surprise: the type saysPoint, the value isn't one. Narrowing withinstanceof(see Narrowing with instanceof) only works for objects really created withnew. typeof Pointin a type position means "the type of the valuePoint" — the constructor function itself, includingnew () => Pointand anystaticmembers. That's whycreatecan callnew ctor().create(Point)passes the class itself (a value) and gets an instance back. If a class hasprivateor#privatemembers, a look-alike literal is no longer accepted — those members make the type behave nominally; see Access modifiers vs #private.
override and implementsTwo keywords help when classes relate to other types. implements checks that a class matches an interface (it adds nothing — see Implementing interfaces in classes). override marks a method that's meant to replace a base-class method, so the compiler can tell you when it doesn't:
class Animal {
move(): string {
return "moving";
}
}
class Dog extends Animal {
override move(): string {
return "running";
}
override mvoe(): string { // ❌ This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'.
return "typo";
}
}override move()says "this replacesAnimal.prototype.move". The checker confirmsAnimalhas amoveand that the new signature is compatible with it (an override that returnednumberwould be an error).override mvoe()is a typo. Withoutoverride, it would compile as a brand-new method anddog.move()would quietly keep running the base version. Withoverride, the checker reports TS4113 because there's nomvoeinAnimalto override.- It also protects you in the other direction: if someone renames
Animal.move, everyoverride movein subclasses becomes an error instead of silently turning into an unrelated method. - Turn on
noImplicitOverrideintsconfig.jsonto make the keyword required — then any method that shadows a base method withoutoverrideis an error too.overrideis erased in the output.