Design Converter
Education
Last updated on Apr 22, 2024
•7 mins read
Last updated on Apr 22, 2024
•7 mins read
Software Development Executive - II
I know who I am.
In TypeScript, when you start thinking about expanding the functionalities of your existing class, you might consider using inheritance. Inheritance allows a new class to adopt the properties and methods of a parent class. This is fundamental when you are trying to extend the capabilities of your code without rewriting existing functionalities.
For example, if you are working on an employee management system, you might have an employee class that you want to extend to include additional details or functionalities.
In TypeScript, extending a single class is straightforward using the ‘extends’ keyword. This keyword helps you create a new class that inherits all the methods and properties from one parent class. Here’s how you might typically use extends to inherit from a parent class:
1class Employee { 2 constructor(public name: string, public department: string) {} 3 logDetails() { 4 console.log(`Name: ${this.name}, Department: ${this.department}`); 5 } 6} 7 8class Manager extends Employee { 9 constructor(name: string, department: string, public reports: string[]) { 10 super(name, department); 11 } 12 logReports() { 13 console.log(this.reports); 14 } 15}
In the above example, the Manager class extends the Employee class. The Manager class is now considered a child class and inherits all the functionalities from the Employee class. It can also have additional properties or methods, such as logReports in this case.
However, TypeScript does not support extending multiple classes directly. This limitation might seem restrictive, especially when your project scales and you want to add additional functionality to an existing class from multiple sources.
For instance, if your Employee class needs to inherit methods from both a Vehicle class detailing the company car and a Module class describing job-specific training, you can't simply extend both classes.
This limitation can be worked around using techniques such as mixins. Mixins allow you to combine methods from multiple classes into a single class. This is particularly useful when you need to inherit functionalities from several sources.
1function CarMixin<Base extends new (...args: any[]) => {}>(Base: Base) { 2 return class extends Base { 3 car: string; 4 constructor(...args: any[]) { 5 super(...args); 6 this.car = 'Honda'; 7 } 8 describeCar() { 9 console.log(`Assigned car: ${this.car}`); 10 } 11 } 12} 13 14interface Employee { 15 name: string; 16 department: string; 17 logDetails: () => void; 18} 19 20const EmployeeWithCar = CarMixin(class Employee { 21 name: string; 22 department: string; 23 constructor(name: string, department: string) { 24 this.name = name; 25 this.department = department; 26 } 27 logDetails() { 28 console.log(`Name: ${this.name}, Department: ${this.department}`); 29 } 30}); 31 32const john = new EmployeeWithCar('John Doe', 'Sales'); 33john.logDetails(); // Logs: Name: John Doe, Department: Sales 34john.describeCar(); // Logs: Assigned car: Honda
In this example, the CarMixin function takes a class and returns a new class that extends it, adding car-related functionalities. This technique enables your Employee class to 'inherit' properties and methods related to a car, even though TypeScript does not natively support extending multiple classes.
Mixins in TypeScript offer a powerful technique for extending the functionalities of classes in a modular and reusable way. A mixin is essentially a function that takes a class as an input and returns a new class that extends the original class with additional properties and methods. This approach allows you to add functionality to existing classes flexibly and dynamically.
Think of mixins as a way to compose classes from multiple sources. They allow you to extend the functionalities of an existing class by mixing in additional behaviors from other classes without the need for direct inheritance.
For example, suppose you have an employee class and you want to add functionalities related to logging activities and managing permissions without altering the original class. In that case, mixins can be the perfect solution.
Implementing mixins in TypeScript involves creating functions that return a class. These functions typically use generics and extend from the constructor of the class you pass in, allowing you to add or modify properties and methods dynamically. Here's a practical example:
1type Constructor<T = {}> = new (...args: any[]) => T; 2 3function Loggable<TBase extends Constructor>(Base: TBase) { 4 return class extends Base { 5 log(text: string) { 6 console.log(`Log entry: ${text}`); 7 } 8 }; 9} 10 11function Permissible<TBase extends Constructor>(Base: TBase) { 12 return class extends Base { 13 permissions: string[] = []; 14 grantPermission(permission: string) { 15 this.permissions.push(permission); 16 } 17 }; 18} 19 20// Apply mixins to the Employee class 21class Employee { 22 constructor(public name: string) {} 23} 24 25const EnhancedEmployee = Loggable(Permissible(Employee)); 26 27let employee = new EnhancedEmployee("Jane Doe"); 28employee.log("Starting shift"); // Works due to Loggable mixin 29employee.grantPermission("edit-time-sheet"); // Works due to Permissible mixin
In the above code, the Loggable and Permissible mixins add logging and permission management functionalities to the Employee class, respectively. This illustrates how mixins can be used to extend the capabilities of classes without modifying their original structure.
In TypeScript, interfaces play a crucial role in structuring and designing robust applications. Interfaces define the shape that objects should follow, acting like contracts in your application's architecture. They are particularly useful in cases where class composition is needed but the limitations of single inheritance are too restrictive.
Here's an example to show how interfaces can be used to ensure that certain classes meet specific structural requirements:
1interface IEmployee { 2 name: string; 3 department: string; 4 logDetails(): void; 5} 6 7interface IManager { 8 teamSize: number; 9 scheduleMeeting: (date: string, room: string) => void; 10} 11 12class Employee implements IEmployee { 13 constructor(public name: string, public department: string) {} 14 15 logDetails() { 16 console.log(`Name: ${this.name}, Department: ${this.department}`); 17 } 18} 19 20class Manager implements IEmployee, IManager { 21 constructor(public name: string, public department: string, public teamSize: number) {} 22 23 logDetails() { 24 console.log(`Name: ${this.name}, Department: ${this.department}, Team Size: ${this.teamSize}`); 25 } 26 27 scheduleMeeting(date: string, room: string) { 28 console.log(`Meeting scheduled on ${date} in ${room}`); 29 } 30}
In this example, the Manager class implements both IEmployee and IManager interfaces, ensuring it contains all the methods and properties defined by these interfaces, thus fulfilling multiple roles through a clear and structured approach.
TypeScript provides powerful ways to combine types, either through intersection or union types. This feature is incredibly beneficial in class composition, allowing you to create complex types that can accept more than one defined type.
• Intersection Types: These allow you to combine multiple types into one. This is useful when you want an object to have both sets of features at the same time.
1type Admin = { 2 rights: string[]; 3}; 4 5type User = { 6 email: string; 7 password: string; 8}; 9 10type AdminUser = Admin & User; 11 12const adminUser: AdminUser = { 13 email: "admin@example.com", 14 password: "admin123", 15 rights: ["create-server", "shutdown-server"] 16}; 17 18console.log(adminUser);
• Union Types: These let a variable be one type OR another. Useful when you need flexibility in your function parameters or when dealing with values that might be of several types.
1type Width = { 2 width: number; 3}; 4 5type Height = { 6 height: number; 7}; 8 9type Dimension = Width | Height; 10 11function setDimension(dim: Dimension) { 12 if ("width" in dim) { 13 console.log(`Width: ${dim.width}`); 14 } else { 15 console.log(`Height: ${dim.height}`); 16 } 17} 18 19setDimension({ width: 50 }); // Outputs "Width: 50" 20setDimension({ height: 20 }); // Outputs "Height: 20"
When dealing with complex class compositions, there are several best practices you should follow to ensure your code remains clean, maintainable, and efficient:
• Do's:
◦ Do use interfaces to ensure structural compatibility.
◦ Do employ type guards when working with union types to ensure correct type usage.
◦ Do document your types and interfaces well to improve code readability and maintainability.
• Don'ts:
◦ Don't overuse intersections and unions as it can lead to complicated and hard-to-maintain code structures.
◦ Don't create large hierarchies of interfaces or types that are hard to follow and understand.
◦ Don't ignore TypeScript's powerful type system; leverage it to catch errors during development.
Throughout this blog, we've explored various techniques for extending and composing classes in TypeScript, from utilizing mixins as flexible enhancers to leveraging interfaces and advanced type operations such as intersections and unions.
Each method offers unique benefits and can be chosen based on the specific needs of your project. By embracing TypeScript's powerful features for class composition, you can build more robust and scalable applications.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.