Skip to main content

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;
}
}