classes-inheritance-and-polymorphism
Basic Class Inheritance Syntax:
In TypeScript, you can extend a class from another, creating a parent-child relationship. It's like getting a recipe from a cookbook and then adding your own twist to it.
class Vehicle {
constructor(public wheels: number) {}
}
class Car extends Vehicle {
constructor() {
super(4); // Cars have 4 wheels
}
}
super
Keyword:
Use super
to refer to the parent class, typically to call the parent's constructor or methods. It's like calling your teacher for help in a classroom.
class Parent {
sayHello() {
return "Hello";
}
}
class Child extends Parent {
sayHello() {
return super.sayHello() + ", world!";
}
}
Method Overriding:
Method overriding allows a subclass to provide a specific implementation of a method already defined by its parent class or interface.
class Printer {
print() {
console.log("Printing from Printer");
}
}
class LaserPrinter extends Printer {
print() {
console.log("Printing from LaserPrinter");
}
}
readonly
Inheritance:
Inherited readonly
properties can be read in the subclass but can’t be changed. They are constants throughout the inheritance chain.
class Animal {
readonly name: string = "Animal";
}
class Dog extends Animal {
bark() {
// this.name = "Dog"; // Error: cannot assign to 'name' because it is a read-only property.
}
}
Abstract Classes:
Abstract classes are like unfinished sculptures. They need a subclass to complete the details.
abstract class Shape {
abstract draw(): void;
}
class Circle extends Shape {
draw() {
console.log("Drawing a circle");
}
}
Abstract Methods:
Subclasses must provide an implementation for any abstract methods inherited from an abstract class, like filling in the blanks of a form.
abstract class Worker {
abstract work(): void;
}
class Developer extends Worker {
work() {
console.log("Writing code");
}
}
Protected Members:
Protected members are like family secrets; they're available to all descendants but hidden from outsiders.
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Employee extends Person {
getName() {
return this.name;
}
}
instanceof
Operator:
The instanceof
operator is like an ID check. It tells you if an object is a member of a particular class.
class Bird {}
class Sparrow extends Bird {}
let sparrow = new Sparrow();
console.log(sparrow instanceof Bird); // true
Polymorphism Basics:
Polymorphism lets you treat a subclass as an instance of a parent class, simplifying code and improving reusability.
class Animal {
makeSound() {}
}
class Dog extends Animal {
makeSound() {
console.log("Bark");
}
}
let pet: Animal = new Dog();
pet.makeSound(); // Outputs: "Bark"
Static Members Inheritance:
Static members, including functions, are shared across a class and its subclasses. It's like a family trait passed from parents to children.
class Base {
static description: string = "Base class";
}
class Derived extends Base {}
console.log(Derived.description); // Output: "Base class"
Parameter Properties:
Parameter properties simplify classes by allowing you to declare and initialize member variables directly in the constructor parameters.
class Car {
constructor(public make: string, public model: string) {}
}
let myCar = new Car("Toyota", "Corolla");
Method Chaining:
Method chaining allows you to call multiple methods on the same object consecutively. Each method returns this
, so you can keep the chain going.
class Calculator {
private result: number = 0;
add(value: number) {
this.result += value;
return this;
}
multiply(value: number) {
this.result *= value;
return this;
}
getResult() {
return this.result;
}
}
let calc = new Calculator();
let total = calc.add(5).multiply(2).getResult(); // Chain methods
Mixins:
Mixins let you create classes that combine methods and properties from multiple sources, like a smoothie with different fruits blended together.
class Disposable {
dispose() {
console.log("Disposing resource");
}
}
class Activatable {
activate() {
console.log("Activating");
}
}
class SmartObject implements Disposable, Activatable {
dispose: () => void;
activate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
Discriminated Unions with Classes:
Discriminated unions work with classes when they share a common property that TypeScript can use to determine the correct type.
class Circle {
kind: "circle";
radius: number;
}
class Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
Type Guards with Classes:
Type guards are like security checks that let you safely determine the specific class of an instance within a class hierarchy.
class Bird {
fly() {
console.log("Flying");
}
}
class Fish {
swim() {
console.log("Swimming");
}
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
animal.fly();
} else {
animal.swim();
}
}
Accessor Overriding:
You can override accessors in TypeScript to refine how properties are set or retrieved in subclasses, like a security checkpoint for your data.
class Person {
private _name: string;
get name() {
return this._name;
}
set name(newName: string) {
this._name = newName;
}
}
class Employee extends Person {
set name(newName: string) {
if (newName.length > 0) {
super.name = newName;
}
}
}
Utility Types and Classes:
Utility types like Partial
or Pick
can be used with classes to transform class instances, giving you fine control over what properties are required.
class Todo {
title: string;
description: string;
}
type TodoPreview = Pick<Todo, "title">;
let todo: TodoPreview = {
title: "Clean room"
};
Method Signatures:
Method signatures must be consistent across subclasses to ensure the principles of polymorphism are followed.
class Animal {
makeSound(input: string) {
console.log(input);
}
}
class Dog extends Animal {
makeSound(input: string) { // Consistent signature
console.log("Bark! " + input);
}
}
Generics and Classes:
Generics allow classes to work with any data type, like a box that can hold any gift, regardless of its shape or size.
class Box<T> {
content: T;
constructor(value: T) {
this.content = value;
}
}
let numberBox = new Box<number>(100);
Decorators:
Decorators wrap classes and class members to add extra behavior, like putting ornaments on a tree to make it look special.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}