Classes
Class Declarations
Section titled “Class Declarations”Fields, Constructors, Methods
Section titled “Fields, Constructors, Methods”A TypeScript class declares fields, a constructor, and methods. Unlike JavaScript, TypeScript Requires that all fields accessed in the class body be declared explicitly (under strictPropertyInitialization).
class Point { x: number; y: number;
constructor(x: number, y: number) { this.x = x; this.y = y; }
distanceTo(other: Point): number { const dx = this.x - other.x; const dy = this.y - other.y; return Math.sqrt(dx * dx + dy * dy); }
toString(): string { return `(${this.x}, ${this.y})`; }}Under strictPropertyInitializationThe compiler verifies that all declared fields are assigned in The constructor or have a definite assignment assertion (!):
class Example { name: string;
constructor() { this.name = "default'; }}
class Lazy { value!: string;
init(): void { this.value = 'initialised'; }}The ! definite assignment assertion tells the compiler that the field will be assigned before use, Even though the constructor does not assign it.
Access Modifiers
Section titled “Access Modifiers”TypeScript supports three access modifiers: public``privateAnd protected.
| Modifier | Accessibility |
|---|---|
public | Accessible from anywhere (default) |
private | Accessible only within the declaring class |
protected | Accessible within the declaring class and its subclasses |
class Account { public owner: string; private balance: number; protected id: string;
constructor(owner: string, initialBalance: number) { this.owner = owner; this.balance = initialBalance; this.id = crypto.randomUUID(); }
public deposit(amount: number): void { if (amount > 0) { this.balance += amount; } }
public withdraw(amount: number): boolean { if (amount > 0 && amount <= this.balance) { this.balance -= amount; return true; } return false; }
public getBalance(): number { return this.balance; }}
class SavingsAccount extends Account { private interestRate: number;
constructor(owner: string, initialBalance: number, rate: number) { super(owner, initialBalance); this.interestRate = rate; }
applyInterest(): void { const balance = this.getBalance(); this.deposit(balance * this.interestRate); }}readonly Modifier
Section titled “readonly Modifier”The readonly modifier prevents assignment to a field after initialisation:
class ImmutablePoint { readonly x: number; readonly y: number;
constructor(x: number, y: number) { this.x = x; this.y = y; }}
const p = new ImmutablePoint(1, 2);p.x = 3;readonly is shallow: it prevents reassignment of the field itself but does not prevent mutation of The field’s value if it is an object.
Parameter Properties
Section titled “Parameter Properties”TypeScript provides a shorthand for declaring and initialising fields in the constructor parameter List. A parameter prefixed with an access modifier is automatically declared as a field and assigned From the constructor argument.
class Point { constructor( public x: number, public y: number, readonly id: string = crypto.randomUUID(), ) {}}
const p = new Point(1, 2);console.log(p.x, p.y, p.id);This is equivalent to:
class Point { public x: number; public y: number; readonly id: string;
constructor(x: number, y: number, id: string = crypto.randomUUID()) { this.x = x; this.y = y; this.id = id; }}Parameter properties can use any combination of modifiers: public``private``protected readonly.
Abstract Classes and Abstract Methods
Section titled “Abstract Classes and Abstract Methods”An abstract class cannot be instantiated directly. It is designed to be extended by concrete Subclasses. Abstract methods have no implementation and must be implemented by subclasses.
abstract class Shape { abstract getArea(): number; abstract getPerimeter(): number;
describe(): string { return `Area: ${this.getArea().toFixed(2)}, Perimeter: ${this.getPerimeter().toFixed(2)}`; }}
class Circle extends Shape { constructor(public readonly radius: number) { super(); }
getArea(): number { return Math.PI * this.radius ** 2; }
getPerimeter(): number { return 2 * Math.PI * this.radius; }}
class Rectangle extends Shape { constructor( public readonly width: number, public readonly height: number, ) { super(); }
getArea(): number { return this.width * this.height; }
getPerimeter(): number { return 2 * (this.width + this.height); }}
function printShape(shape: Shape): void { console.log(shape.describe());}
printShape(new Circle(5));printShape(new Rectangle(3, 4));
const s = new Shape();The last line is a compile error because Shape is abstract.
Abstract Fields
Section titled “Abstract Fields”TypeScript 4.2+ supports abstract property declarations:
abstract class Base { abstract name: string; abstract readonly id: string;}
class Derived extends Base { name = 'derived'; readonly id = '001';}Implementing Interfaces
Section titled “Implementing Interfaces”Classes implement interfaces using the implements keyword. A class may implement multiple Interfaces. The class must provide concrete implementations for all members declared in the Interfaces.
interface Serializable { serialize(): string;}
interface Deserializable<T> { deserialize(json: string): T;}
class User implements Serializable { constructor( public name: string, public email: string, ) {}
serialize(): string { return JSON.stringify({ name: this.name, email: this.email }); }}
class UserFactory implements Deserializable<User> { deserialize(json: string): User { const data = JSON.parse(json); return new User(data.name, data.email); }}Interface vs Abstract Class
Section titled “Interface vs Abstract Class”| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple implementation | A class can implement many interfaces | A class can extend only one abstract class |
| Instance fields | Cannot have instance fields (only declarations) | Can have instance fields |
| Constructor | No constructor | Can have a constructor |
| Method implementations | All methods are abstract (no body) | Can mix abstract and concrete methods |
| Access modifiers | No access modifiers on members | Supports public``private``protected |
Static Members and Static Factory Patterns
Section titled “Static Members and Static Factory Patterns”Static Fields and Methods
Section titled “Static Fields and Methods”Static members belong to the class itself, not to instances. They are accessed via the class name.
class IdGenerator { private static counter = 0;
static nextId(): number { return ++IdGenerator.counter; }
static reset(): void { IdGenerator.counter = 0; }}
console.log(IdGenerator.nextId());console.log(IdGenerator.nextId());Static Factory Pattern
Section titled “Static Factory Pattern”class LogEntry { private constructor( public readonly level: "info'' | "warn' | 'error', public readonly message: string, public readonly timestamp: Date = new Date(), ) {}
static info(message: string): LogEntry { return new LogEntry('info', message); }
static warn(message: string): LogEntry { return new LogEntry('warn', message); }
static error(message: string): LogEntry { return new LogEntry('error', message); }}
const entry = LogEntry.info('Server started');Making the constructor private ensures that instances can only be created through the static factory Methods.
Static Members in Interfaces
Section titled “Static Members in Interfaces”TypeScript allows static members in interfaces using the typeof pattern:
interface Constructor<T> { new (...args: any[]): T;}
function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T { return new ctor(...args);}
const point = createInstance(Point, 1, 2);Getter and Setter Accessors
Section titled “Getter and Setter Accessors”TypeScript supports getter and setter accessor methods using the get and set keywords:
class Temperature { private _celsius: number;
constructor(celsius: number) { this._celsius = celsius; }
get celsius(): number { return this._celsius; }
set celsius(value: number) { if (value < -273.15) { throw new Error('Temperature below absolute zero'); } this._celsius = value; }
get fahrenheit(): number { return (this._celsius * 9) / 5 + 32; }
set fahrenheit(value: number) { this.celsius = ((value - 32) * 5) / 9; }}
const temp = new Temperature(0);console.log(temp.fahrenheit);temp.fahrenheit = 212;console.log(temp.celsius);Common Pitfall: A getter without a setter produces a readonly-like effect at runtime but the Field is not readonly in the type system. If a setter exists but is omitted from a subclass, the Getter is still accessible.
Class Expressions vs Class Declarations
Section titled “Class Expressions vs Class Declarations”Class Declaration
Section titled “Class Declaration”class Foo { method(): void {}}Class Expression
Section titled “Class Expression”const Bar = class { method(): void {}};Class expressions are useful for one-off classes or when the class is used as a value (e.g., passed To a function):
function createClass(methodBody: string) { return class { execute(): void { console.log(methodBody); } };}
const Dynamic = createClass('hello');new Dynamic().execute();Mixins and Composition with Classes
Section titled “Mixins and Composition with Classes”TypeScript does not have a native mixin keyword, but mixins can be implemented using a combination Of class expressions, intersection types, and generic factory functions.
Mixin Pattern
Section titled “Mixin Pattern”function Timestamped<T extends new (...args: any[]) => any>(Base: T) { return class extends Base { createdAt = new Date();
getAge(): number { return Date.now() - this.createdAt.getTime(); } };}
function Identifiable<T extends new (...args: any[]) => any>(Base: T) { return class extends Base { readonly id = crypto.randomUUID(); };}
class BaseEntity { constructor(public name: string) {}}
const TimedEntity = Timestamped(BaseEntity);const IdEntity = Identifiable(TimedEntity);
const entity = new IdEntity('test');console.log(entity.id, entity.createdAt, entity.name);Mixin Type Pattern
Section titled “Mixin Type Pattern”To type the resulting class correctly, use an intersection type:
function Serializable<T extends new (...args: any[]) => {}>(Base: T) { return class extends Base { serialize(): string { return JSON.stringify(this); } };}
class User extends Serializable(BaseEntity) {}
type SerializedUser = User & { serialize(): string };Composition Over Inheritance
Section titled “Composition Over Inheritance”Composition is generally preferred over inheritance for flexibility:
interface Loggable { log(message: string): void;}
interface Validatable<T> { validate(data: T): string[];}
class ConsoleLogger implements Loggable { log(message: string): void { console.log(`[${new Date().toISOString()}] ${message}`); }}
class FormValidator<T extends object> implements Validatable<T> { constructor(private rules: Partial<Record<keyof T, (v: any) => boolean>>) {}
validate(data: T): string[] { const errors: string[] = []; for (const key in this.rules) { if (!this.rules[key]?.(data[key])) { errors.push(`Invalid ${String(key)}`); } } return errors; }}
class RegistrationService { constructor( private logger: Loggable, private validator: Validatable<RegistrationData>, ) {}
register(data: RegistrationData): boolean { const errors = this.validator.validate(data); if (errors.length > 0) { this.logger.log(`Validation failed: ${errors.join(', ')}`); return false; } this.logger.log('Registration successful'); return true; }}
interface RegistrationData { email: string; password: string; age: number;}Decorators
Section titled “Decorators”Decorators are a Stage 3 ECMAScript proposal. TypeScript supports an experimental implementation of Decorators via the experimentalDecorators compiler option. As of TypeScript 5.0, the language also Supports the TC39 Stage 3 decorator proposal.
Method Decorators
Section titled “Method Decorators”function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) { console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`); const result = originalMethod.apply(this, args); console.log(`${propertyKey} returned: ${result}`); return result; };
return descriptor;}
class Calculator { @log add(a: number, b: number): number { return a + b; }}
new Calculator().add(2, 3);Class Decorators
Section titled “Class Decorators”function sealed<T extends new (...args: any[]) => {}>(constructor: T) { Object.seal(constructor); Object.seal(constructor.prototype);}
@sealedclass ImmutableService { method(): void {}}Property Decorators
Section titled “Property Decorators”function readonly(target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.writable = false; return descriptor;}
class Config { @readonly version = '1.0.0';}Common Pitfall: The experimentalDecorators flag and the TC39 Stage 3 proposal use different Decorator semantics. Code written for one will not work with the other. New projects should prefer The TC39 Stage 3 proposal (no experimentalDecorators flag needed in TS 5.0+).
Structural Typing and Nominal Typing for Classes
Section titled “Structural Typing and Nominal Typing for Classes”Structural Typing (Default)
Section titled “Structural Typing (Default)”TypeScript uses structural typing for classes. Two classes are compatible if they have the same Shape, regardless of their declaration heritage:
class Point2D { constructor( public x: number, public y: number, ) {}}
class Vector2D { constructor( public x: number, public y: number, ) {}}
const p: Point2D = new Vector2D(1, 2);This compiles because Vector2D has the same public shape as Point2D.
Simulating Nominal Typing with Branded Types
Section titled “Simulating Nominal Typing with Branded Types”When nominal typing is required (i.e., type compatibility based on the name rather than the shape), Use branded types:
type Brand<T, B extends string> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;type OrderId = Brand<string, 'OrderId'>;
function createUserId(id: string): UserId { return id as UserId;}
function createOrderId(id: string): OrderId { return id as OrderId;}
function getUser(id: UserId): void {}function getOrder(id: OrderId): void {}
const userId = createUserId('u1');const orderId = createOrderId('o1');
getUser(userId);getUser(orderId);The last call is a compile error because OrderId is not assignable to UserIdEven though both Are branded string types.
Private Fields and Structural Typing
Section titled “Private Fields and Structural Typing”Private fields affect structural typing. Two classes with incompatible private fields are not Structurally compatible, even if their public shapes are identical:
class Secret { private secret = 'hidden'; public value = 42;}
class NotSecret { private secret = 'different'; public value = 42;}
const a: Secret = new NotSecret();This is a compile error because the private secret field has a different origin class.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Forgetting to Call super()
Section titled “Pitfall 1: Forgetting to Call super()”In derived classes, super() must be called before accessing this in the constructor:
class Derived extends Base { constructor() { this.value = 0; super(); }}This is an error. super() must be called first.
Pitfall 2: Structural Typing Surprises
Section titled “Pitfall 2: Structural Typing Surprises”Because of structural typing, classes with the same public shape are interchangeable. This can lead To subtle bugs when the classes have different internal behaviour:
class Celsius { constructor(public value: number) {}}
class Fahrenheit { constructor(public value: number) {}}
function printTemp(temp: Celsius): void { console.log(`${temp.value} C`);}
printTemp(new Fahrenheit(100));This compiles but produces misleading output.
Pitfall 3: this in Callbacks
Section titled “Pitfall 3: this in Callbacks”Passing a class method as a callback detaches it from the instance:
class Timer { private count = 0;
start(): void { setInterval(this.tick, 1000); }
tick(): void { this.count++; }}
new Timer().start();At runtime, this inside tick will be undefined (in strict mode) because the method was Extracted from its object. Use arrow functions or bind:
start(): void { setInterval(() => this.tick(), 1000);}Summary
Section titled “Summary”This topic covers the core concepts of classes, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Intuition
Section titled “Intuition”TypeScript classes combine JavaScript’s class syntax with compile-time type checking. Access modifiers (public, private, protected) control visibility, while readonly prevents reassignment. Abstract classes define contracts that subclasses must implement. The key distinction from nominal languages is structural typing: two classes are compatible if they have the same shape, regardless of their inheritance chain. This means you can simulate nominal typing with branded types when you need identity-based compatibility.
Cross-References
Section titled “Cross-References”- [[typescript/types-and-annotations]] - Interface versus class trade-offs
- [[typescript/generics]] - Generic classes and factory patterns
- [[typescript/functions]] - Method signatures and this parameter typing
- [[typescript/enums-and-modules]] - Module patterns for organizing class hierarchies