0% found this document useful (0 votes)
43 views

10 Advance TypeScript Concept

These concepts enhance TypeScript’s type system, enabling robust and maintainable code.

Uploaded by

psamcyitedev
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
43 views

10 Advance TypeScript Concept

These concepts enhance TypeScript’s type system, enabling robust and maintainable code.

Uploaded by

psamcyitedev
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 12

102


9913111

Niharika Goulikar
Posted on 14. Nov.
50
13
12
12
12

10 Advanced TypeScript Concepts


Every Developer Should Know
#typescript#webdev#javascript#programming
TypeScript is a modern programming language often preferred over
JavaScript for its added type safety. In this article, I'll share the top 10
TypeScript concepts that will help sharpen your TypeScript programming
skills.Are you ready?Let's go.

1.Generics : Using generics we can create reusable types, which will be


helpful in dealing with the data of the today as well as the data of the
tomorrow.
Example of Generics:
We might want a function in Typescript that takes an argument as some type,
and we might want to return the same type.
function func<T>(args:T):T{
return args;
}

2.Generics with Type Constraints : Now let's limit the type T by defining it to
accept only strings and integers:
function func<T extends string | number>(value: T): T {
return value;
}

const stringValue = func("Hello"); // Works, T is string


const numberValue = func(42); // Works, T is number
// const booleanValue = func(true); // Error: Type 'boolean' is not assignable to type 'string |
number'

3.Generic Interfaces:
Interface generics are useful when you want to define contracts (shapes) for
objects, classes, or functions that work with a variety of types. They allow you
to define a blueprint that can adapt to different data types while keeping the
structure consistent.
// Generic interface with type parameters T and U
interface Repository<T, U> {
items: T[]; // Array of items of type T
add(item: T): void; // Function to add an item of type T
getById(id: U): T | undefined; // Function to get an item by ID of type U
}

// Implementing the Repository interface for a User entity


interface User {
id: number;
name: string;
}

class UserRepository implements Repository<User, number> {


items: User[] = [];

add(item: User): void {


this.items.push(item);
}

getById(idOrName: number | string): User | undefined {


if (typeof idOrName === 'string') {
// Search by name if idOrName is a string
console.log('Searching by name:', idOrName);
return this.items.find(user => user.name === idOrName);
} else if (typeof idOrName === 'number') {
// Search by id if idOrName is a number
console.log('Searching by id:', idOrName);
return this.items.find(user => user.id === idOrName);
}
return undefined; // Return undefined if no match found
}
}

// Usage
const userRepo = new UserRepository();
userRepo.add({ id: 1, name: "Alice" });
userRepo.add({ id: 2, name: "Bob" });

const user1 = userRepo.getById(1);


const user2 = userRepo.getById("Bob");
console.log(user1); // Output: { id: 1, name: "Alice" }
console.log(user2); // Output: { id: 2, name: "Bob" }

4.Generic Classes:: Use this when you want all the properties in your class
to adhere to the type specified by the generic parameter. This allows for
flexibility while ensuring that every property of the class matches the type
passed to the class.
interface User {
id: number;
name: string;
age: number;
}

class UserDetails<T extends User> {


id: T['id'];
name: T['name'];
age: T['age'];

constructor(user: T) {
this.id = user.id;
this.name = user.name;
this.age = user.age;
}

// Method to get user details


getUserDetails(): string {
return `User: ${this.name}, ID: ${this.id}, Age: ${this.age}`;
}

// Method to update user name


updateName(newName: string): void {
this.name = newName;
}

// Method to update user age


updateAge(newAge: number): void {
this.age = newAge;
}
}

// Using the UserDetails class with a User type


const user: User = { id: 1, name: "Alice", age: 30 };
const userDetails = new UserDetails(user);

console.log(userDetails.getUserDetails()); // Output: "User: Alice, ID: 1, Age: 30"

// Updating user details


userDetails.updateName("Bob");
userDetails.updateAge(35);

console.log(userDetails.getUserDetails()); // Output: "User: Bob, ID: 1, Age: 35"


console.log(new UserDetails("30")); // Error: "This will throw error"

5.Constraining Type Parameters to Passed Types: At times, we want to a


parameter type to depend on some other passed parameters.Sounds
confusing,let's see the example below.
function getProperty<Type>(obj: Type, key: keyof Type) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // Valid
getProperty(x, "d"); // Error: Argument of type '"d"' is not assignable to parameter of type '"a" |
"b" | "c"'.

6.Conditional Types : Often, we want our types to be either one type or


another. In such situations, we use conditional types.
A Simple example would be:
function func(param:number|boolean){
return param;
}
console.log(func(2)) //Output: 2 will be printed
console.log(func("True")) //Error: boolean cannot be passed as argument

A little bit complex example:


type HasProperty<T, K extends keyof T> = K extends "age" ? "Has Age" : "Has Name";
interface User {
name: string;
age: number;
}

let test1: HasProperty<User, "age">; // "Has Age"


let test2: HasProperty<User, "name">; // "Has Name"
let test3: HasProperty<User, "email">; // Error: Type '"email"' is not assignable to parameter of
type '"age" | "name"'.

6.Intersection Types: These types are useful when we want to combine


multiple types into one, allowing a particular type to inherit properties and
behaviors from various other types.
Let's see an interesting example for this:
// Defining the types for each area of well-being

interface MentalWellness {
mindfulnessPractice: boolean;
stressLevel: number; // Scale of 1 to 10
}

interface PhysicalWellness {
exerciseFrequency: string; // e.g., "daily", "weekly"
sleepDuration: number; // in hours
}

interface Productivity {
tasksCompleted: number;
focusLevel: number; // Scale of 1 to 10
}

// Combining all three areas into a single type using intersection types
type HealthyBody = MentalWellness & PhysicalWellness & Productivity;

// Example of a person with a balanced healthy body


const person: HealthyBody = {
mindfulnessPractice: true,
stressLevel: 4,
exerciseFrequency: "daily",
sleepDuration: 7,
tasksCompleted: 15,
focusLevel: 8
};

// Displaying the information


console.log(person);

7.infer keyword: The infer keyword is useful when we want to conditionally


determine a specific type, and when the condition is met, it allows us to
extract subtypes from that type.
This is the general syntax:
type ConditionalType<T> = T extends SomeType ? InferredType : OtherType;

Example for this:


type ReturnTypeOfPromise<T> = T extends Promise<infer U> ? U : number;

type Result = ReturnTypeOfPromise<Promise<string>>; // Result is 'string'


type ErrorResult = ReturnTypeOfPromise<number>; // ErrorResult is 'never'

const result: Result = "Hello";


console.log(typeof result); // Output: 'string'

8.Type Variance : This concept talks how subtype and supertype are related
to each other.
These are of two types:
Covariance: A subtype can be used where a supertype is expected.
Let's see an example for this:
class Vehicle { }
class Car extends Vehicle { }

function getCar(): Vehicle {


return new Car();
}

function getVehicle(): Vehicle {


return new Vehicle();
}

// Covariant assignment
let car: Car = getCar();
let vehicle: Vehicle = getVehicle(); // This works because Car is a subtype of Vehicle
In the above example, Car has inherited properties from Vehicle class,so it's
absolutely valid to assign it to subtype where supertype is expected as
subtype would be having all the properties that a supertype has.
Contravariance: This is opposite of covariance.We use supertypes in places
where subType is expected to be.
class Vehicle {
startEngine() {
console.log("Vehicle engine starts");
}
}

class Car extends Vehicle {


honk() {
console.log("Car honks");
}
}

function processVehicle(vehicle: Vehicle) {


vehicle.startEngine(); // This works
// vehicle.honk(); // Error: 'honk' does not exist on type 'Vehicle'
}

function processCar(car: Car) {


car.startEngine(); // Works because Car extends Vehicle
car.honk(); // Works because 'Car' has 'honk'
}

let car: Car = new Car();


processVehicle(car); // This works because of contravariance (Car can be used as Vehicle)
processCar(car); // This works as well because car is of type Car

// Contravariance failure if you expect specific subtype behavior in the method

When using contravariance, we need to be cautious not to access properties


or methods that are specific to the subtype, as this may result in an error.
9. Reflections: This concept involves determining the type of a variable at
runtime. While TypeScript primarily focuses on type checking at compile time,
we can still leverage TypeScript operators to inspect types during runtime.
typeof operator : We can make use of typeof operator to find the type of
variable at the runtime
const num = 23;
console.log(typeof num); // "number"

const flag = true;


console.log(typeof flag); // "boolean"

instanceof Operator: The instanceof operator can be used to check if an


object is an instance of a class or a particular type.
class Vehicle {
model: string;
constructor(model: string) {
this.model = model;
}
}

const benz = new Vehicle("Mercedes-Benz");


console.log(benz instanceof Vehicle); // true

We can use third-party library to determine types at the runtime.


10.Dependency Injection: Dependency Injection is a pattern that allows you
to bring code into your component without actually creating or managing it
there. While it may seem like using a library, it's different because you don’t
need to install or import it via a CDN or API.
At first glance, it might also seem similar to using functions for reusability, as
both allow for code reuse. However, if we use functions directly in our
components, it can lead to tight coupling between them. This means that any
change in the function or its logic could impact every place it is used.
Dependency Injection solves this problem by decoupling the creation of
dependencies from the components that use them, making the code more
maintainable and testable.
Example without dependency injection
// Health-related service classes without interfaces
class MentalWellness {
getMentalWellnessAdvice(): string {
return "Take time to meditate and relax your mind.";
}
}

class PhysicalWellness {
getPhysicalWellnessAdvice(): string {
return "Make sure to exercise daily for at least 30 minutes.";
}
}

// HealthAdvice class directly creating instances of the services


class HealthAdvice {
private mentalWellnessService: MentalWellness;
private physicalWellnessService: PhysicalWellness;

// Directly creating instances inside the class constructor


constructor() {
this.mentalWellnessService = new MentalWellness();
this.physicalWellnessService = new PhysicalWellness();
}

// Method to get both mental and physical wellness advice


getHealthAdvice(): string {
return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also,
${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
}
}

// Creating an instance of HealthAdvice, which itself creates instances of the services


const healthAdvice = new HealthAdvice();

console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at
least 30 minutes."

Example with Dependecy Injection


// Health-related service interfaces with "I" prefix
interface IMentalWellnessService {
getMentalWellnessAdvice(): string;
}

interface IPhysicalWellnessService {
getPhysicalWellnessAdvice(): string;
}
// Implementations of the services
class MentalWellness implements IMentalWellnessService {
getMentalWellnessAdvice(): string {
return "Take time to meditate and relax your mind.";
}
}

class PhysicalWellness implements IPhysicalWellnessService {


getPhysicalWellnessAdvice(): string {
return "Make sure to exercise daily for at least 30 minutes.";
}
}

// HealthAdvice class that depends on services via interfaces


class HealthAdvice {
private mentalWellnessService: IMentalWellnessService;
private physicalWellnessService: IPhysicalWellnessService;

// Dependency injection via constructor


constructor(
mentalWellnessService: IMentalWellnessService,
physicalWellnessService: IPhysicalWellnessService
){
this.mentalWellnessService = mentalWellnessService;
this.physicalWellnessService = physicalWellnessService;
}

// Method to get both mental and physical wellness advice


getHealthAdvice(): string {
return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also,
${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
}
}

// Dependency injection
const mentalWellness: IMentalWellnessService = new MentalWellness();
const physicalWellness: IPhysicalWellnessService = new PhysicalWellness();

// Injecting services into the HealthAdvice class


const healthAdvice = new HealthAdvice(mentalWellness, physicalWellness);

console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at
least 30 minutes."
In a tightly coupled scenario, if you have a stressLevel attribute in the
MentalWellness class today and decide to change it to something else
tomorrow, you would need to update all the places where it was used. This
can lead to a lot of refactoring and maintenance challenges.

However, with dependency injection and the use of interfaces, you can avoid
this problem. By passing the dependencies (such as the MentalWellness
service) through the constructor, the specific implementation details (like the
stressLevel attribute) are abstracted away behind the interface. This means
that changes to the attribute or class do not require modifications in the
dependent classes, as long as the interface remains the same. This approach
ensures that the code is loosely coupled, more maintainable, and easier to
test, as you’re injecting what’s needed at runtime without tightly coupling
components.

You might also like