Classes, Access Modifiers, and OOP
Use classes, constructors, inheritance, abstract classes, interfaces, and parameter properties appropriately.
Classes and Constructors
TypeScript classes compile to JavaScript class syntax according to the configured target. A constructor initializes an instance, and fields can be typed explicitly or inferred. implements checks that a class satisfies a contract; it does not add runtime methods or enforce the interface at runtime.
Use classes when identity, lifecycle, or encapsulated behavior is useful. For plain data transfer objects, interfaces and object literals are often simpler. Remember that class types and instance values are not the same thing: typeof SomeClass refers to the constructor side, while SomeClass in a type position generally describes instances.
interface Clock {
now(): Date;
}
class SystemClock implements Clock {
now(): Date {
return new Date();
}
}
const clock: Clock = new SystemClock();SystemClock satisfies the Clock contract because it has a compatible now method. The interface is a compile-time contract and does not exist as a runtime object.- Use classes for behavior and encapsulated state.
implementschecks compatibility but adds no runtime behavior.- Distinguish instance types from constructor/static types.
Does `implements Clock` automatically generate missing methods?
Access Modifiers and Parameter Properties
TypeScript supports public, protected, and private modifiers, plus readonly. public is the default. protected allows access in the class and subclasses; TypeScript's private restricts access through the type system, while JavaScript's #private fields provide runtime privacy. Parameter properties let a constructor parameter declare and initialize a field in concise syntax.
Use the least permissive access that fits the design. Do not confuse compile-time access modifiers with security boundaries: client-side code and emitted JavaScript can be inspected, and TypeScript's private keyword alone is not a runtime security feature.
class Account {
constructor(
public readonly id: string,
private balance: number
) {}
getBalance(): number {
return this.balance;
}
}
const account = new Account("acct-1", 250);id cannot be reassigned through this type; balance is only accessible inside the class according to TypeScript's access checking.publicis the default visibility.- TypeScript
privateis not the same as JavaScript#privateruntime privacy. - Use readonly for fields that should not be reassigned after initialization.
Which feature provides JavaScript runtime private-field semantics?
Inheritance and Abstract Classes
Inheritance models an “is-a” relationship and allows a subclass to specialize a base class. An abstract class cannot be instantiated directly and may require subclasses to implement abstract members. Interfaces are often preferable when only a contract is needed and shared implementation is unnecessary.
Avoid deep inheritance hierarchies that make behavior hard to trace. Composition—building a behavior from smaller collaborators—is often easier to test and evolve. If a base class has many protected hooks or subclasses need to know internal details, reconsider whether the abstraction is too tightly coupled.
abstract class NotificationChannel {
abstract send(message: string): Promise<void>;
protected format(message: string): string {
return `[Bytes4Dev] ${message}`;
}
}
class ConsoleChannel extends NotificationChannel {
async send(message: string): Promise<void> {
console.log(this.format(message));
}
}- Abstract classes can combine contracts and shared behavior.
- Interfaces are suitable for contracts without implementation.
- Prefer composition over unnecessarily deep inheritance.
Can an abstract class be instantiated directly?