TypeScript supports ES2015 class syntax and adds types, visibility modifiers, and more. (Source: Classes.)
Fields and constructors
class Point {
x: number;
y: number;
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
const pt = new Point();
console.log(pt.x, pt.y); // 0 0
const pt2 = new Point(5, 10);
console.log(pt2.x, pt2.y); // 5 10
pt.x = 3;
pt.y = 4;
console.log(pt.x, pt.y); // 3 4
Point declares two number fields and a constructor that sets them. Default parameters (x = 0) mean new Point() is valid and starts at (0, 0). Pass arguments with new Point(5, 10) to set custom coordinates. After construction you can read and write pt.x and pt.y like normal properties.
Initializers can infer types:
class Point {
x = 0;
y = 0;
}
const origin = new Point();
console.log(origin.x, origin.y); // 0 0
origin.x = 12;
origin.y = 8;
console.log(origin.x, origin.y); // 12 8
Assigning 0 at the field declaration tells TypeScript both fields are number. You do not need an explicit : number annotation. new Point() still creates a usable instance; you can change x and y afterward.
With strictPropertyInitialization (part of strict), fields must be initialized in the declaration or constructor:
class BadGreeter {
name: string; // Error: not definitely assigned
}
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
const greeter = new GoodGreeter();
console.log(greeter.name); // "hello"
greeter.name = "hi there";
console.log(greeter.name); // "hi there"
BadGreeter fails because name might be used before it has a value. GoodGreeter assigns name in the constructor, so TypeScript is satisfied. After new GoodGreeter(), you can read and update greeter.name like any public field.
If something else assigns the field, use the definite assignment assertion:
class OKGreeter {
name!: string; // you promise it will be set
}
const ok = new OKGreeter();
ok.name = "assigned later";
console.log(ok.name); // "assigned later"
The ! after name tells TypeScript: "I will assign this before use, even if you cannot see it here." You still create the instance with new OKGreeter(), then set name yourself. Use it carefully. A wrong promise becomes a runtime undefined bug.
readonly
Assignable in the constructor only:
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
}
const defaultGreeter = new Greeter();
console.log(defaultGreeter.name); // "world"
const customGreeter = new Greeter("TypeScript");
console.log(customGreeter.name); // "TypeScript"
// customGreeter.name = "Nope"; // Error: readonly
readonly fields can be set in the declaration or constructor, then never again. new Greeter() keeps the default "world". new Greeter("TypeScript") sets the name once in the constructor. Reading name is fine; assigning after construction is a compile error.
Methods and accessors
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
const point = new Point();
console.log(point.x, point.y); // 10 10
point.scale(2);
console.log(point.x, point.y); // 20 20
point.scale(0.5);
console.log(point.x, point.y); // 10 10
scale is a normal instance method. Create a Point with new Point(), then call point.scale(2) to multiply both coordinates. Each call updates the same instance’s x and y.
class C {
_length = 0;
get length() {
return this._length;
}
set length(value: number) {
this._length = value;
}
}
const box = new C();
console.log(box.length); // 0
box.length = 25;
console.log(box.length); // 25
box.length = box.length + 5;
console.log(box.length); // 30
The get / set pair lets you write box.length like a property while still controlling reads and writes. new C() starts at 0. Assigning box.length = 25 runs the setter; reading box.length runs the getter. Always use this. for instance members inside methods.
implements and extends
implements checks that a class matches an interface (it does not change the class’s inferred types):
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// Error: missing ping()
pong() {
console.log("pong!");
}
}
const sonar = new Sonar();
sonar.ping(); // "ping!"
const devices: Pingable[] = [new Sonar()];
devices[0].ping(); // "ping!"
Sonar satisfies Pingable because it has ping(). Create it with new Sonar() and call sonar.ping(). You can also store instances in a Pingable[] and call ping() through the interface type. Ball does not implement ping(), so TypeScript errors even though Ball has other methods. implements is a checklist, not inheritance of behavior.
extends inherits members; call super() before using this in a derived constructor:
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
d.move(); // "Moving along!" (from Animal)
d.woof(3); // "woof!" three times
const pets: Animal[] = [new Dog(), new Animal()];
pets[0].move();
Dog inherits move from Animal and adds woof. new Dog() gives you both methods on the same instance. You can also put a Dog in an Animal[] because a dog is an animal. This is classic inheritance: shared behavior in the base class, specialized behavior in the subclass.
Override carefully : the derived method must stay compatible with the base:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const derived = new Derived();
derived.greet(); // "Hello, world!" (calls super.greet)
derived.greet("ada"); // "Hello, ADA"
const asBase: Base = new Derived();
asBase.greet(); // still works with zero args
Create a Derived with new Derived(). Call greet() with no args to reuse the parent message, or pass a name for the custom path. Assigning the instance to a Base variable still works because the override accepts zero arguments. Making name required would break that. super.greet() reuses the parent implementation.
Member visibility
| Modifier | Who can access |
|---|---|
public (default) |
Anywhere |
protected |
Class + subclasses |
private |
Only that class (TypeScript-only) |
#field |
Hard private at runtime (JS private fields) |
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
console.log("Howdy, " + this.getName()); // OK
}
}
const g = new SpecialGreeter();
g.greet(); // "Hello, hi"
g.howdy(); // "Howdy, hi"
// g.getName(); // Error: protected
new SpecialGreeter() creates a subclass instance. Public methods greet and howdy are callable from outside. getName is protected, so the subclass may use it inside howdy, but code outside the class hierarchy cannot call g.getName(). That keeps helper methods internal while still allowing inheritance.
class Base {
private x = 0;
showX() {
console.log(this.x); // OK inside the class
}
}
const base = new Base();
base.showX(); // 0
// console.log(base.x); // Error at compile time
Create the instance with new Base(). Public methods like showX() can read the private field and print it. Direct access like base.x is blocked at compile time. private / protected are erased in the emitted JavaScript. For true runtime privacy, use #privateField.
Parameter properties
Shortcut: declare and assign fields in the constructor parameter list:
class Person {
constructor(
public name: string,
private age: number,
) {}
describe() {
return `${this.name} is ${this.age} years old`;
}
}
const p = new Person("Ada", 36);
console.log(p.name); // "Ada"
console.log(p.describe()); // "Ada is 36 years old"
// console.log(p.age); // Error: private
public name and private age both declare a field and assign the argument in one step. new Person("Ada", 36) creates the instance with both values set. You can read p.name and call p.describe() (which uses private age internally). You cannot read p.age from outside.
Static members
Belong to the class, not an instance:
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
instanceLabel = "I am an instance";
}
MyClass.printX(); // 0
MyClass.x = 5;
MyClass.printX(); // 5
const a = new MyClass();
const b = new MyClass();
console.log(a.instanceLabel); // "I am an instance"
console.log(b.instanceLabel); // "I am an instance"
// a.printX(); // Error: static members are not on the instance
Call static members on the class itself (MyClass.printX()), not on an instance. Changing MyClass.x is shared by the whole class. new MyClass() still creates normal instances for non-static fields like instanceLabel. Each instance gets its own copy of those. Static members cannot use the class’s generic type parameters (there is only one static slot at runtime).
Abstract classes
Cannot be constructed directly; subclasses must implement abstract members:
abstract class Shape {
abstract getArea(): number;
describe() {
return `Area is ${this.getArea()}`;
}
}
class Square extends Shape {
constructor(private sideLength: number) {
super();
}
getArea() {
return this.sideLength ** 2;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea() {
return Math.PI * this.radius ** 2;
}
}
// new Shape(); // Error
const square = new Square(42);
console.log(square.getArea()); // 1764
console.log(square.describe()); // "Area is 1764"
const circle = new Circle(3);
console.log(circle.getArea()); // about 28.27
console.log(circle.describe()); // "Area is ..."
const shapes: Shape[] = [square, circle];
for (const shape of shapes) {
console.log(shape.describe());
}
Shape defines a shared describe method and requires subclasses to provide getArea. You cannot new Shape(). Create concrete instances with new Square(42) or new Circle(3), then call getArea() or describe() on them. You can also store different shapes in a Shape[] and call the shared API. Use abstract classes when you want shared code plus a required override.
Practice
- Build a
BankAccountwith aprivatebalance,deposit/withdraw, and areadonlyaccount id. Create an instance and call the methods. - Extend it with
SavingsAccountthat adds interest via aprotectedhelper. Create a savings account and apply interest. - Add an
interface Drawable { draw(): void }and a class thatimplementsit. Put instances in aDrawable[]and calldraw()on each.