Understanding Classes in TypeScript
Classes in TypeScript define the blueprint of an object, including its properties and methods. With TypeScript’s added typing support, classes are more robust, enabling developers to write type-safe, predictable code. Here’s a basic example of a TypeScript class:
class User {
private id: number;
public name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
displayInfo() {
return `User: ${this.name} (ID: ${this.id})`;
}
}
// Usage
const user = new User(1, "Alice");
console.log(user.displayInfo()); // Output: User: Alice (ID: 1)
This `User` class has private and public properties. The `id` property is private, ensuring it cannot be accessed outside the class, while `name` is public and accessible from outside the class instance.
Working with Interfaces in TypeScript
Interfaces in TypeScript define the structure that classes or objects must follow, acting as a contract that ensures consistency. Interfaces are particularly useful for establishing reusable types across multiple classes or objects:
interface Product {
id: number;
name: string;
price: number;
display(): string;
}
class Book implements Product {
constructor(public id: number, public name: string, public price: number) {}
display() {
return `Book: ${this.name} costs $${this.price}`;
}
}
// Usage
const book: Product = new Book(101, "TypeScript Essentials", 29.99);
console.log(book.display()); // Output: Book: TypeScript Essentials costs $29.99
Here, the `Book` class implements the `Product` interface, ensuring that it provides all the properties and methods specified by the interface. This approach enforces consistency, making the code easier to understand and maintain.
Combining Classes and Interfaces for Flexibility
Classes and interfaces work well together in TypeScript. While classes provide concrete implementations, interfaces ensure that classes follow specific structures. In many applications, it’s beneficial to use interfaces to define core data structures and then create classes that implement these interfaces:
interface Vehicle {
brand: string;
speed: number;
accelerate(): void;
}
class Car implements Vehicle {
constructor(public brand: string, public speed: number) {}
accelerate() {
this.speed += 10;
console.log(`${this.brand} is now going at ${this.speed} km/h`);
}
}
// Usage
const car = new Car("Toyota", 120);
car.accelerate(); // Output: Toyota is now going at 130 km/h
By using an interface (`Vehicle`) to define the core properties and methods, any class implementing `Vehicle` will maintain a consistent structure. This allows for flexibility and predictability across different implementations.
Advanced TypeScript Features: Extending Classes and Interfaces
TypeScript allows for extending both classes and interfaces, making it possible to create complex hierarchies and shared structures. Here’s an example demonstrating class inheritance and interface extension:
Extending Classes
class Animal {
constructor(public name: string) {}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
// Usage
const dog = new Dog("Buddy");
dog.speak(); // Output: Buddy barks.
In this example, `Dog` extends the `Animal` class and overrides the `speak` method. Class inheritance allows for specialized behavior in subclasses while still using the base class’s core functionality.
Extending Interfaces
interface Shape {
area(): number;
}
interface ColoredShape extends Shape {
color: string;
}
class Circle implements ColoredShape {
constructor(public radius: number, public color: string) {}
area() {
return Math.PI * this.radius * this.radius;
}
}
// Usage
const circle = new Circle(5, "red");
console.log(`Circle Area: ${circle.area()} with color: ${circle.color}`);
// Output: Circle Area: 78.54 with color: red
In this example, `ColoredShape` extends `Shape`, adding a new `color` property. The `Circle` class then implements `ColoredShape`, which requires it to provide both `area` and `color`, ensuring that all necessary details are covered.
Best Practices for Using Classes and Interfaces
Here are some best practices for working with classes and interfaces in TypeScript:
- Use Interfaces for Type Safety: Use interfaces to define the structure of objects and classes, ensuring consistency and reducing the likelihood of errors.
- Encapsulate Class Properties: Use access modifiers like `private` and `protected` to control access to class properties, improving code reliability and security.
- Leverage Composition over Inheritance: While inheritance is useful, composition (using interfaces) often leads to more flexible and maintainable code.
When to Use Classes vs. Interfaces in TypeScript
In general, use interfaces to define types and structure, particularly when working with object shapes or function signatures. Use classes for entities that require functionality (methods) along with data:
- Interfaces: Ideal for defining object types, contracts for classes, or representing shapes without behavior.
- Classes: Best suited for entities that have behavior (methods), especially when encapsulation or initialization logic is needed.
Conclusion
Classes and interfaces in TypeScript provide powerful tools for organizing and enforcing structure within your code. By understanding how to leverage interfaces for consistency and classes for functionality, you can build scalable, maintainable, and type-safe applications. Using TypeScript’s advanced features, such as inheritance and extension, enables developers to create flexible architectures suited to complex project needs.