TypeScriptClasses Overview

TypeScript Classes

TypeScript enhances JavaScript classes with type annotations, access modifiers, abstract classes, and generic classes that provide powerful object-oriented programming patterns while keeping full compatibility with JavaScript runtime behaviour.

Class Syntax and Field Declarations

TypeScript requires you to declare class fields (with their types) before using them. This gives the compiler full knowledge of an instance's shape.

TS
class User {
  id: number;
  name: string;
  email: string;

  constructor(id: number, name: string, email: string) {
    this.id    = id;
    this.name  = name;
    this.email = email;
  }
}

const alice = new User(1, 'Alice', 'alice@example.com');
console.log(alice.name); // 'Alice'

// TypeScript catches typos and wrong types immediately
// alice.nmae = 'Bob';  // Error: Property 'nmae' does not exist
Note
Unlike JavaScript, TypeScript requires you to declare class fields before using them in the constructor when strictPropertyInitialization is enabled.
TypeScript Classes vs JavaScript Classes

At runtime, TypeScript classes compile to the same JavaScript class syntax — TypeScript adds nothing to the runtime. All the extras (access modifiers, readonly, generics) exist only in the type layer and are erased during compilation.

Feature

TypeScript

JavaScript

Type annotations

Yes — erased at compile time

No

Access modifiers (public/private/protected)

Yes — compile-time only

No (use # for runtime private)

Abstract classes

Yes

No

Readonly fields

Yes — compile-time only

No (use Object.freeze)

Generic classes

Yes — erased at compile time

No

Interface implementation

Yes (implements keyword)

No

Parameter properties

Yes (shorthand)

No

Tip
If you need runtime private fields (truly inaccessible in JavaScript), use the # syntax. TypeScript private is only a compile-time guard.
Methods with Type Annotations

TS
class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width  = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }

  perimeter(): number {
    return 2 * (this.width + this.height);
  }

  scale(factor: number): Rectangle {
    return new Rectangle(this.width * factor, this.height * factor);
  }

  toString(): string {
    return `Rectangle(${this.width} x ${this.height})`;
  }
}

const r = new Rectangle(4, 6);
console.log(r.area());        // 24
console.log(r.perimeter());   // 20
console.log(r.scale(2).area()); // 96
Implementing Interfaces

A class can declare that it satisfies one or more interfaces with the implements keyword. TypeScript then verifies that all required members are present and correctly typed.

TS
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

interface Printable {
  print(): void;
}

class Document implements Serializable, Printable {
  content: string;

  constructor(content: string) {
    this.content = content;
  }

  serialize(): string {
    return JSON.stringify({ content: this.content });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.content = parsed.content;
  }

  print(): void {
    console.log(this.content);
  }
}

const doc = new Document('Hello, TypeScript!');
doc.print();                    // Hello, TypeScript!
const json = doc.serialize();   // '{"content":"Hello, TypeScript!"}'
doc.deserialize('{"content":"Updated"}');
doc.print();                    // Updated
Note
implements is purely a compile-time check. It does not change how the class works at runtime and does not add any code to the output.
Abstract Classes

An abstract class is a base class that cannot be instantiated directly. It can define abstract methods (signatures without a body) that subclasses must implement, as well as concrete methods that subclasses inherit.

TS
abstract class Shape {
  // Abstract method — no body, must be implemented by subclasses
  abstract area(): number;
  abstract perimeter(): number;

  // Concrete method — shared by all shapes
  describe(): string {
    return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`;
  }
}

// const s = new Shape(); // Error: Cannot create an instance of an abstract class

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

class Triangle extends Shape {
  constructor(private a: number, private b: number, private c: number) {
    super();
  }

  area(): number {
    const s = (this.a + this.b + this.c) / 2;
    return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
  }

  perimeter(): number {
    return this.a + this.b + this.c;
  }
}

const shapes: Shape[] = [new Circle(5), new Triangle(3, 4, 5)];
shapes.forEach(s => console.log(s.describe()));
Area: 78.54, Perimeter: 31.42
Area: 6.00, Perimeter: 12
Static Members

Static members belong to the class itself, not to any instance. They are accessed through the class name, not this.

TS
class Counter {
  private static count: number = 0;

  // Static factory method
  static create(): Counter {
    return new Counter();
  }

  static getCount(): number {
    return Counter.count;
  }

  static reset(): void {
    Counter.count = 0;
  }

  constructor() {
    Counter.count++;
  }

  increment(): void {
    Counter.count++;
  }
}

const c1 = Counter.create();
const c2 = Counter.create();
const c3 = new Counter();

console.log(Counter.getCount()); // 3
c1.increment();
console.log(Counter.getCount()); // 4
Counter.reset();
console.log(Counter.getCount()); // 0
Readonly Fields

The readonly modifier prevents a field from being reassigned after it is set in the constructor. It is a compile-time check — not a runtime freeze.

TS
class Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  distanceTo(other: Point): number {
    return Math.hypot(this.x - other.x, this.y - other.y);
  }

  translate(dx: number, dy: number): Point {
    // Cannot mutate — return a new Point instead
    return new Point(this.x + dx, this.y + dy);
  }
}

const origin = new Point(0, 0);
const p      = new Point(3, 4);

console.log(p.distanceTo(origin)); // 5
// p.x = 10; // Error: Cannot assign to 'x' because it is a read-only property

const moved = p.translate(1, 1);
console.log(`(${moved.x}, ${moved.y})`); // (4, 5)
Generic Classes

Classes can be generic, allowing you to write type-safe data structures that work with any type. The type parameter is declared after the class name and can be used throughout all methods and fields.

TS
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.peek()); // 3
console.log(numberStack.pop());  // 3
console.log(numberStack.size);   // 2

const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.pop()); // 'world'
Generic Classes with Constraints

TS
interface Identifiable {
  id: number;
}

class Repository<T extends Identifiable> {
  private store = new Map<number, T>();

  save(entity: T): void {
    this.store.set(entity.id, entity);
  }

  findById(id: number): T | undefined {
    return this.store.get(id);
  }

  findAll(): T[] {
    return Array.from(this.store.values());
  }

  delete(id: number): boolean {
    return this.store.delete(id);
  }
}

type Product = Identifiable & { name: string; price: number };

const repo = new Repository<Product>();
repo.save({ id: 1, name: 'Widget', price: 9.99 });
repo.save({ id: 2, name: 'Gadget', price: 19.99 });

console.log(repo.findById(1));  // { id: 1, name: 'Widget', price: 9.99 }
console.log(repo.findAll().length); // 2
Success
Generic classes are one of TypeScript's most powerful features. They let you write data structures and patterns (Repository, Stack, Queue, Cache) that are fully type-safe for any type you choose.
Class Hierarchy Example

Putting it all together: abstract base, interface implementation, generics, and static members in one coherent hierarchy.

TS
interface Logger {
  log(message: string): void;
}

abstract class BaseService implements Logger {
  private static instanceCount = 0;
  readonly serviceId: number;

  constructor(protected name: string) {
    BaseService.instanceCount++;
    this.serviceId = BaseService.instanceCount;
  }

  log(message: string): void {
    console.log(`[${this.name}#${this.serviceId}] ${message}`);
  }

  abstract execute(): void;

  static getInstanceCount(): number {
    return BaseService.instanceCount;
  }
}

class EmailService extends BaseService {
  constructor(private recipient: string) {
    super('EmailService');
  }

  execute(): void {
    this.log(`Sending email to ${this.recipient}`);
  }
}

class SmsService extends BaseService {
  constructor(private phoneNumber: string) {
    super('SmsService');
  }

  execute(): void {
    this.log(`Sending SMS to ${this.phoneNumber}`);
  }
}

const email = new EmailService('user@example.com');
const sms   = new SmsService('+1-555-0100');

email.execute(); // [EmailService#1] Sending email to user@example.com
sms.execute();   // [SmsService#2] Sending SMS to +1-555-0100

console.log('Total services:', BaseService.getInstanceCount()); // 2