Classes & OOP
TypeScript adds powerful features to JavaScript classes: access modifiers, abstract classes, and more. It makes object-oriented programming feel natural and type-safe.
1. Class Basics
Basic Class Structure
class User {
// Properties
name: string;
email: string;
// Constructor
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
// Methods
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
const user = new User('Alice', '[email protected]');
console.log(user.greet()); // "Hello, I'm Alice"
Parameter Properties (Shorthand)
TypeScript can automatically create and assign properties from constructor parameters:
class User {
// public keyword creates and assigns the property
constructor(
public name: string,
public email: string,
public age: number = 0
) {}
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
// Equivalent to the longer version above
const user = new User('Alice', '[email protected]', 25);
2. Access Modifiers
Control visibility of class members.
public (Default)
Accessible from anywhere.
class User {
public name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User('Alice');
console.log(user.name); // ✅ Accessible
private
Only accessible within the class.
class BankAccount {
private balance: number = 0;
deposit(amount: number): void {
this.balance += amount;
}
withdraw(amount: number): boolean {
if (amount > this.balance) return false;
this.balance -= amount;
return true;
}
getBalance(): number {
return this.balance;
}
}
const account = new BankAccount();
account.deposit(100);
// account.balance; // ❌ Error: Property 'balance' is private
console.log(account.getBalance()); // 100
protected
Accessible within the class and subclasses.
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark(): string {
return `${this.name} says Woof!`; // ✅ Can access protected
}
}
const dog = new Dog('Max');
// dog.name; // ❌ Error: Property 'name' is protected
console.log(dog.bark()); // "Max says Woof!"
Private Fields (ES2022)
True private fields using # syntax.
class Counter {
#count = 0; // Truly private (runtime enforcement)
increment(): number {
return ++this.#count;
}
get value(): number {
return this.#count;
}
}
const counter = new Counter();
counter.increment();
// counter.#count; // ❌ Syntax error - truly inaccessible
console.log(counter.value); // 1
private vs #: private is TypeScript-only (compile-time). # is JavaScript-native (runtime-enforced). Use # for true encapsulation.
3. Readonly Properties
Properties that can only be set in the constructor.
class User {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
changeName(name: string): void {
this.name = name; // ✅ OK
// this.id = 999; // ❌ Error: Cannot assign to 'id' because it is read-only
}
}
Combining with Parameter Properties
class User {
constructor(
public readonly id: number,
public name: string
) {}
}
4. Getters and Setters
Define computed properties with validation.
class Circle {
private _radius: number;
constructor(radius: number) {
this._radius = radius;
}
// Getter
get radius(): number {
return this._radius;
}
// Setter with validation
set radius(value: number) {
if (value <= 0) {
throw new Error('Radius must be positive');
}
this._radius = value;
}
// Computed property (getter only)
get area(): number {
return Math.PI * this._radius ** 2;
}
get circumference(): number {
return 2 * Math.PI * this._radius;
}
}
const circle = new Circle(5);
console.log(circle.area); // 78.54...
circle.radius = 10; // Uses setter
console.log(circle.circumference); // 62.83...
// circle.area = 100; // ❌ Error: Cannot set - no setter
5. Static Members
Properties and methods on the class itself, not instances.
class MathUtils {
static PI = 3.14159;
static add(a: number, b: number): number {
return a + b;
}
static multiply(a: number, b: number): number {
return a * b;
}
// Static block (TS 4.4+)
static {
console.log('MathUtils class initialized');
}
}
// Access without instantiation
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(2, 3)); // 5
// Cannot access on instance
const utils = new MathUtils();
// utils.PI; // ❌ Error
Singleton Pattern
class Database {
private static instance: Database;
private constructor() {} // Private constructor prevents new Database()
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance
6. Inheritance
Extend classes to create specialized versions.
class Animal {
constructor(public name: string) {}
move(distance: number): void {
console.log(`${this.name} moved ${distance}m`);
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Call parent constructor
}
bark(): void {
console.log(`${this.name} says Woof!`);
}
// Override parent method
move(distance: number): void {
console.log('Running...');
super.move(distance); // Call parent method
}
}
const dog = new Dog('Max', 'Golden Retriever');
dog.bark(); // "Max says Woof!"
dog.move(10); // "Running..." then "Max moved 10m"
Method Overriding with override
class Animal {
speak(): void {
console.log('Some sound');
}
}
class Dog extends Animal {
override speak(): void { // Explicitly marks override
console.log('Woof!');
}
// override bark(): void {} // ❌ Error: no 'bark' in parent
}
7. Abstract Classes
Define templates for subclasses. Cannot be instantiated directly.
abstract class Shape {
constructor(public color: string) {}
// Abstract method - must be implemented by subclasses
abstract getArea(): number;
abstract getPerimeter(): number;
// Concrete method - can be inherited
describe(): string {
return `A ${this.color} shape with area ${this.getArea()}`;
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
color: string,
public width: number,
public height: number
) {
super(color);
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// const shape = new Shape('red'); // ❌ Error: Cannot create instance of abstract class
const circle = new Circle('red', 5);
console.log(circle.describe()); // "A red shape with area 78.54..."
const rect = new Rectangle('blue', 4, 6);
console.log(rect.getArea()); // 24
8. Implementing Interfaces
Classes can implement one or more interfaces.
interface Printable {
print(): void;
}
interface Loggable {
log(): void;
}
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
class Document implements Printable, Loggable, Serializable {
constructor(public content: string) {}
print(): void {
console.log(`Printing: ${this.content}`);
}
log(): void {
console.log(`Logging: ${this.content}`);
}
serialize(): string {
return JSON.stringify({ content: this.content });
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
this.content = parsed.content;
}
}
Interface vs Abstract Class
| Feature | Interface | Abstract Class |
|---|
| Multiple inheritance | ✅ Yes | ❌ No |
| Implementation code | ❌ No | ✅ Yes |
| Properties with values | ❌ No | ✅ Yes |
| Constructor | ❌ No | ✅ Yes |
| Access modifiers | ❌ No | ✅ Yes |
9. Generic Classes
Classes that work with multiple types.
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];
}
isEmpty(): boolean {
return this.items.length === 0;
}
get size(): number {
return this.items.length;
}
}
// Type-safe stacks
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push('hello');
stringStack.push('world');
console.log(stringStack.peek()); // 'world'
Generic Constraints in Classes
interface Identifiable {
id: number;
}
class Repository<T extends Identifiable> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
remove(id: number): boolean {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
return true;
}
return false;
}
}
interface User extends Identifiable {
name: string;
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'Alice' });
console.log(userRepo.findById(1)); // { id: 1, name: 'Alice' }
10. Decorators
Decorators are a stage 3 proposal for JavaScript. TypeScript has experimental support.
Enable decorators in tsconfig.json:{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Class Decorator
function Logger(constructor: Function) {
console.log(`Creating instance of ${constructor.name}`);
}
@Logger
class User {
constructor(public name: string) {}
}
const user = new User('Alice'); // Logs: "Creating instance of User"
Decorator Factory
function Logger(prefix: string) {
return function (constructor: Function) {
console.log(`${prefix}: ${constructor.name}`);
};
}
@Logger('INFO')
class User {
constructor(public name: string) {}
}
Method Decorator
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;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Logs: "Calling add with args: [2,3]"
// Logs: "add returned: 5"
Property Decorator
function Required(target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
if (newVal === undefined || newVal === null || newVal === '') {
throw new Error(`${propertyKey} is required`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Required
name!: string;
}
const user = new User();
// user.name = ''; // ❌ Throws: "name is required"
user.name = 'Alice'; // ✅ OK
11. Practical Example: Service Layer
// Interfaces
interface Entity {
id: number;
createdAt: Date;
updatedAt: Date;
}
interface UserEntity extends Entity {
name: string;
email: string;
role: 'admin' | 'user';
}
// Base Repository
abstract class BaseRepository<T extends Entity> {
protected items: T[] = [];
abstract create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): T;
findAll(): T[] {
return [...this.items];
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
update(id: number, data: Partial<T>): T | undefined {
const index = this.items.findIndex(item => item.id === id);
if (index === -1) return undefined;
this.items[index] = {
...this.items[index],
...data,
updatedAt: new Date()
};
return this.items[index];
}
delete(id: number): boolean {
const index = this.items.findIndex(item => item.id === id);
if (index === -1) return false;
this.items.splice(index, 1);
return true;
}
}
// User Repository
class UserRepository extends BaseRepository<UserEntity> {
private nextId = 1;
create(data: Omit<UserEntity, 'id' | 'createdAt' | 'updatedAt'>): UserEntity {
const user: UserEntity = {
...data,
id: this.nextId++,
createdAt: new Date(),
updatedAt: new Date()
};
this.items.push(user);
return user;
}
findByEmail(email: string): UserEntity | undefined {
return this.items.find(user => user.email === email);
}
findByRole(role: 'admin' | 'user'): UserEntity[] {
return this.items.filter(user => user.role === role);
}
}
// Usage
const userRepo = new UserRepository();
const alice = userRepo.create({
name: 'Alice',
email: '[email protected]',
role: 'admin'
});
const bob = userRepo.create({
name: 'Bob',
email: '[email protected]',
role: 'user'
});
console.log(userRepo.findByRole('admin')); // [alice]
console.log(userRepo.findByEmail('[email protected]')); // bob
Summary
| Concept | Example |
|---|
| Class Definition | class User { name: string } |
| Constructor | constructor(name: string) {} |
| Parameter Property | constructor(public name: string) {} |
| Access Modifiers | public, private, protected |
| Private Fields | #privateField |
| Readonly | readonly id: number |
| Getters/Setters | get value(), set value(v) |
| Static Members | static PI = 3.14 |
| Inheritance | class Dog extends Animal |
| Abstract Class | abstract class Shape |
| Implement Interface | class Doc implements Printable |
| Generic Class | class Stack<T> |
| Decorators | @Logger class User |
Next, we’ll dive deep into advanced types!