Site icon Blogs – Nexotips

Elevate Your TypeScript Skills for 2024 : Classes and Precise Type Narrowing

Type Narrowing and classes

In TypeScript Type Narrowing is a statically typed superset of JavaScript that adds optional type annotations, interfaces, and other features to the language. It is designed to make the development process more robust by catching errors early through static analysis. One of the powerful features of TypeScript is its support for classes and type narrowing, which helps in writing cleaner and more maintainable code.

The Basics of TypeScript Classes

Classes are the blueprints for creating objects in TypeScript. They encapsulate data and behavior, making it easier to manage and reuse code. TypeScript builds on the class syntax introduced in ES6, adding more robust type checking and object-oriented programming features.

class Person {
name: string;
age: number;

constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

greet(): void {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}

const person = new Person('Alice', 30);
person.greet();

In this example:

Inheritance in TypeScript

TypeScript supports inheritance, allowing classes to extend other classes. This promotes code reuse and helps organize related functionalities.

class Employee extends Person {
position: string;

constructor(name: string, age: number, position: string) {
super(name, age);
this.position = position;
}

describeJob(): void {
console.log(`I am a ${this.position}.`);
}
}

const employee = new Employee('Bob', 25, 'Developer');
employee.greet();
employee.describeJob();

Access Modifiers

TypeScript provides three access modifiers: public, private, and protected, which control the visibility of class members.

class Animal {
public name: string;
protected age: number;
private type: string;

constructor(name: string, age: number, type: string) {
this.name = name;
this.age = age;
this.type = type;
}

public getType(): string {
return this.type;
}
}

class Dog extends Animal {
constructor(name: string, age: number) {
super(name, age, 'Dog');
}

public describe(): void {
console.log(`This is a ${this.getType()} named ${this.name} and it is ${this.age} years old.`);
}
}

const dog = new Dog('Rex', 5);
dog.describe();

In this example:

Static Properties and Methods

Static properties and methods belong to the class itself rather than to instances of the class.

class MathUtils {
static PI: number = 3.14;

static calculateCircumference(diameter: number): number {
return this.PI * diameter;
}
}

console.log(MathUtils.PI);
console.log(MathUtils.calculateCircumference(10));

Abstract Classes and Methods

Abstract classes and methods offer a means to establish a framework for derived classes. Direct instantiation of abstract classes is not possible, and they may include abstract methods that subclasses are required to implement.

abstract class Shape {
abstract getArea(): number;

describe(): void {
console.log(`This shape has an area of ${this.getArea()}.`);
}
}

class Circle extends Shape {
radius: number;

constructor(radius: number) {
super();
this.radius = radius;
}

getArea(): number {
return Math.PI * this.radius * this.radius;
}
}

const circle = new Circle(5);
circle.describe();

In this example:

Type Narrowing in TypeScript

Type narrowing refers to refining the type of a variable within a specific block of code, based on certain conditions. This makes TypeScript’s type system more powerful and enables more precise type checks.

Type Guards

Type guards are conditional checks that narrow down the type of a variable within a specific scope.

typeof Type Guard
function printId(id: number | string): void {
if (typeof id === 'string') {
console.log(`ID in uppercase: ${id.toUpperCase()}`);
} else {
console.log(`ID doubled: ${id * 2}`);
}
}

printId('abc');
printId(123);
instanceof Type Guard
class Cat {
meow(): void {
console.log('Meow!');
}
}

class Dog {
bark(): void {
console.log('Woof!');
}
}

function makeNoise(animal: Cat | Dog): void {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.bark();
}
}

makeNoise(new Cat());
makeNoise(new Dog());
in Type Guard
interface Fish {
swim(): void;
}

interface Bird {
fly(): void;
}

function move(animal: Fish | Bird): void {
if ('swim' in animal) {
animal.swim();
} else {
animal.fly();
}
}

const fish: Fish = { swim: () => console.log('Swimming...') };
const bird: Bird = { fly: () => console.log('Flying...') };

move(fish);
move(bird);

Type Predicates

Type predicates are functions that return a boolean value and provide a type assertion to TypeScript. They are useful for creating custom type guards.

function isString(value: any): value is string {
return typeof value === 'string';
}

function printValue(value: string | number): void {
if (isString(value)) {
console.log(`String value: ${value}`);
} else {
console.log(`Number value: ${value}`);
}
}

printValue('hello');
printValue(42);

Discriminated Unions

Discriminated unions are a pattern that combines union types with a common literal property (the discriminator) to enable more precise type narrowing.

interface Square {
kind: 'square';
size: number;
}

interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}

interface Circle {
kind: 'circle';
radius: number;
}

type Shape = Square | Rectangle | Circle;

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'square':
return shape.size * shape.size;
case 'rectangle':
return shape.width * shape.height;
case 'circle':
return Math.PI * shape.radius * shape.radius;
}
}

const square: Square = { kind: 'square', size: 5 };
const rectangle: Rectangle = { kind: 'rectangle', width: 10, height: 20 };
const circle: Circle = { kind: 'circle', radius: 7 };

console.log(`Square area: ${getArea(square)}`);
console.log(`Rectangle area: ${getArea(rectangle)}`);
console.log(`Circle area: ${getArea(circle)}`);

In this example:

Advanced Concepts in TypeScript Classes and Narrowing

Intersection Types

Intersection types combine multiple types into one. This is useful when you want to ensure that a value meets multiple type constraints.

interface Drivable {
drive(): void;
}

interface Flyable {
fly(): void;
}

type FlyingCar = Drivable & Flyable;

class FutureCar implements FlyingCar {
drive(): void {
console.log('Driving...');
}

fly(): void {
console.log('Flying...');
}
}

const myCar = new FutureCar();
myCar.drive();
myCar.fly();

Mapped Types

Mapped types transform existing types into new ones, allowing you to create types dynamically.

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

interface Point {
x: number;
y: number;
}

const point: Readonly<Point> = { x: 10, y: 20 };

// point.x = 15; // Error: Cannot assign to 'x' because it is a read-only property.

Conditional Types

Conditional types enable type-level logic, allowing you to choose types based on conditions.

type IsNumber<T> = T extends number ? 'yes' : 'no';

type A = IsNumber<number>; // 'yes'
type B = IsNumber<string>; // 'no'

In this example:

Utility Types

TypeScript offers a range of pre-defined utility types that assist in performing typical type conversions.

interface Todo {
title: string;
description: string;
completed: boolean;
}

type PartialTodo = Partial<Todo>;
type RequiredTodo = Required<Todo>;
type ReadonlyTodo = Readonly<Todo>;
type PickTodo = Pick<Todo, 'title' | 'completed'>;
type OmitTodo = Omit<Todo, 'description'>;

Conclusion

TypeScript classes and type narrowing are powerful features that enhance the language’s capabilities, making it easier to write robust and maintainable code. By understanding and effectively using these features, you can take full advantage of TypeScript’s static type system and object-oriented programming capabilities. Whether you are defining complex class hierarchies, implementing custom type guards, or leveraging advanced type transformations, TypeScript provides the tools you need to build sophisticated applications with confidence.

Read More : TypeScript Triumph 2024 : Advance Your Development Game

Exit mobile version