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.
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 existstrictPropertyInitialization 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 |
# syntax. TypeScript private is only a compile-time guard.Methods with Type Annotations
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()); // 96Implementing 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.
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(); // Updatedimplements 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.
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.
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()); // 0Readonly 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.
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.
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
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); // 2Class Hierarchy Example
Putting it all together: abstract base, interface implementation, generics, and static members in one coherent hierarchy.
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