TypeScript classes build on JavaScript classes and add type checking for properties, constructor parameters, method parameters, return values, visibility, and interface implementation.
Classes are useful when data and behavior naturally belong together. They are common in services, domain models, framework code, dependency injection, and object-oriented designs. For plain data with no behavior, an interface or type alias is usually simpler.
class Course {
title: string;
lessons: number;
constructor(title: string, lessons: number) {
this.title = title;
this.lessons = lessons;
}
summary(): string {
return `${this.title} has ${this.lessons} lessons`;
}
}
const course = new Course("TypeScript", 15);
console.log(course.summary());
With strict class checking enabled, TypeScript expects class properties to be initialized before they are used. You can initialize properties where they are declared, assign them in the constructor, or make them optional when they may be missing.
This catches a common JavaScript bug: creating an object where a method assumes a property exists, but the constructor never assigned it.
class UserSession {
userId: number;
createdAt: Date = new Date();
lastSeenAt?: Date;
constructor(userId: number) {
this.userId = userId;
}
touch(): void {
this.lastSeenAt = new Date();
}
}
Parameter properties are a TypeScript shortcut. Adding public, private, protected, or readonly to a constructor parameter automatically creates and assigns a class property.
This pattern removes boilerplate, but it should still be readable. For complex initialization, regular constructor code may be clearer.
class UserService {
constructor(
private readonly apiUrl: string,
public readonly cacheEnabled: boolean
) {}
endpoint(path: string): string {
return `${this.apiUrl}/${path}`;
}
}
const service = new UserService("https://api.example.com", true);
console.log(service.cacheEnabled);
Access modifiers describe where a class member can be used. public members are available everywhere. private members are available only inside the class. protected members are available inside the class and subclasses.
Use visibility to protect internal implementation details. A smaller public surface is easier to maintain because fewer outside files depend on class internals.
| Modifier | Where It Can Be Used |
|---|---|
public | Anywhere |
private | Inside the declaring class only |
protected | Inside the class and subclasses |
readonly | Assigned once, then not reassigned |
class Counter {
private value = 0;
increment(): void {
this.value++;
}
current(): number {
return this.value;
}
}
const counter = new Counter();
counter.increment();
console.log(counter.current());
Getters and setters let a class expose property-like access while still running logic. A getter computes or returns a value. A setter validates or transforms input before storing it.
Use them when property syntax improves readability. If an operation is expensive, asynchronous, or has side effects, a normal method is usually clearer.
class Product {
constructor(
public readonly name: string,
private amount: number
) {}
get price(): number {
return this.amount;
}
set price(value: number) {
if (value < 0) {
throw new Error("price cannot be negative");
}
this.amount = value;
}
}
A class can implement an interface to promise that it provides a required shape. This is useful when different classes should be interchangeable through the same contract.
The interface checks the public side of the class. Private implementation details can change as long as the class still satisfies the public contract.
interface Repository<T> {
findById(id: number): T | null;
}
type User = { id: number; name: string };
class UserRepository implements Repository<User> {
private users: User[] = [{ id: 1, name: "Admin" }];
findById(id: number): User | null {
return this.users.find(user => user.id === id) ?? null;
}
}
Classes can extend other classes. The subclass inherits public and protected members and can override methods. Use inheritance only when the relationship is genuinely "is a" and shared behavior belongs in a base class.
Deep inheritance trees are hard to maintain. Prefer composition when a class simply needs to use another object to do part of its work.
class BaseService {
protected log(message: string): void {
console.log(`[service] ${message}`);
}
}
class BlogService extends BaseService {
publish(title: string): void {
this.log(`published ${title}`);
}
}
Static members belong to the class itself, not to an instance. They are useful for factories, constants, and helper methods that are closely related to the class concept.
Avoid using static members as a place for hidden global state. Hidden shared state can make tests and application behavior harder to reason about.
class ApiUrl {
static readonly defaultBase = "https://api.example.com";
static join(path: string): string {
return `${ApiUrl.defaultBase}/${path.replace(/^\\//, "")}`;
}
}
console.log(ApiUrl.join("/users"));
Do not use classes only because they are available. Many TypeScript programs are cleanly written with functions, plain objects, and modules. Use classes when identity, lifecycle, inheritance, dependency injection, or method-based behavior makes the design clearer.
For simple data containers, an interface or type alias is often enough. For stateful services or models with behavior, a class may be the right tool.
Explore 500+ free tutorials across 20+ languages and frameworks.