10 Advanced TypeScript Concepts Every Developer Should Know
Niharika Goulikar

Niharika Goulikar @niharikaa

About: Software Engineer

Location:
India
Joined:
Aug 6, 2024

10 Advanced TypeScript Concepts Every Developer Should Know

Publish Date: Nov 14 '24
510 41

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.

Let's go catty

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;
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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"'.

Enter fullscreen mode Exit fullscreen mode

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: string cannot be passed as argument here

Enter fullscreen mode Exit fullscreen mode

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"'.

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 {
  start() {
    console.log("Vehicle is running");
  }
}

class Car extends Vehicle {
  honk() {
    console.log("this vehicle honks");
  }
}

function vehiclefunc(vehicle: Vehicle) {
  vehicle.start(); 
}

function carfunc(car: Car) {
  car.start();        // Works because Car extends Vehicle(inheritance)
  car.honk();        // Works because 'Car' has 'honk'
}

let car: Car = new Car();
vehiclefunc(car); // Allowed due to covariance
Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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."

Enter fullscreen mode Exit fullscreen mode

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."


Enter fullscreen mode Exit fullscreen mode

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.

Comments 41 total

  • K Om Senapati
    K Om Senapati Nov 14, 2024

    Awesome detailed explanation 🔥

  • Anisa
    AnisaNov 14, 2024

    Loved the explanation on dependency injection!

  • Akshaya Goulikar
    Akshaya GoulikarNov 14, 2024

    Very helpful!Thanks for the article.

  • Harshika
    HarshikaNov 14, 2024

    Amazing article!

  • Viswanath R
    Viswanath RNov 14, 2024

    Great Article!

  • Martins Gouveia
    Martins GouveiaNov 14, 2024

    Thanks to your article, I was able to understand D.I

  • Rohan Sharma
    Rohan SharmaNov 14, 2024

    Thanks for sharing such a detailed blog!

  • Dilpreet Grover
    Dilpreet GroverNov 14, 2024

    Cool!

  • Rohith Singh
    Rohith SinghNov 14, 2024

    great article and great explanation too👏

  • Sohail SJ | TheZenLabs
    Sohail SJ | TheZenLabsNov 14, 2024

    HasProperty was cool to know! But in what context I can use it?

    • Eric Bieszczad-Stie
      Eric Bieszczad-StieNov 15, 2024

      Here's an example where I had to use it. I was using a component library in Nuxt.js for a table. It took in two paramters; data and columns. The columns parameter was a list of objects with a property label of type string and a property key of type string. The key described which properties to read for that specific column. For example:

      type Data = {
          data: {
              name: {
                  first: string;
                  last: string;
              }
          }
      }
      
      columns = [
          {label: "Name", key="data.name.first"} // ok
          {label: "Age", key="data.age"} // error, data.age does not exist
      ]
      
      <Table :data="somedata: Data" :columns="columns" />
      
      Enter fullscreen mode Exit fullscreen mode

      It's a similar problem, but slightly more advanced because you need to create a recursive type as well as do some string literal types in typescripts type system (not javascript).

  • José Pablo Ramírez Vargas
    José Pablo Ramírez VargasNov 14, 2024

    The covariance example is incorrect. Who else can spot it? I see many comments on the wonders of the article, many reactions too, but nobody notices it? Hmmm. People, what's up?

  • Eric Bieszczad-Stie
    Eric Bieszczad-StieNov 15, 2024

    Nice list, but this is wrong;

    While TypeScript primarily focuses on type checking at compile time, we can still leverage TypeScript operators to inspect types during runtime.

    TypeScript is only a static type checker that checks at compile time (or rather transpilation time).

    typeof is a JavaScript feature, not a TypeScript feature. Even though javascript does not have strict static typing, every language must still have the concept of types at some point because hardware instructions are different for say string operations and numerical operations. Typescripts type system is something separate from that. It's ONLY a static checker performed before code is run, and TypeScript-specific code features will never run at runtime.

  • Markus Zeller
    Markus ZellerNov 15, 2024

    console.log(func("True")) //Error: boolean cannot be passed as argument is not correct. "True" is a string, but boolean is declared as acceptable in here function func(param:number|boolean). Comments lie. Am I an liar?

    • Niharika Goulikar
      Niharika GoulikarNov 15, 2024

      Image description
      I didn't get you

      • Markus Zeller
        Markus ZellerNov 15, 2024

        The string "true" is not a the boolean type true.
        typeof("true") !== typeof(true)

        Image description

        • Niharika Goulikar
          Niharika GoulikarNov 15, 2024

          I know I wanted to show that it we will get error when we pass "True" when we are only supposed to pass either string or number.

          • Markus Zeller
            Markus ZellerNov 15, 2024

            I think you refer the wrong part. I am talking about 6. Conditional Types.

            Quoting:

            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
            
            Enter fullscreen mode Exit fullscreen mode

            **number **and **boolean **are allowed, but you pass a string. The comment is wrong telling a **boolean **can not be passed. Same for using "True" as string pretending a true boolean is very misleading and terrible wording.

            • Niharika Goulikar
              Niharika GoulikarNov 15, 2024

              Ahhh!Let me fix the comment.
              Thanks for reading!

              • Yulia Lantzberg
                Yulia LantzbergNov 15, 2024

                @niharikaa you should fix the comment. It is "Error: string cannot be passed as argument. " Boolean is one of the optional parameters in the function. "True" is the string, it is not a boolean. true without quotes and without capital letter is a boolean

                • Markus Zeller
                  Markus ZellerNov 15, 2024

                  You're welcome and thanks for the article.

  • Abdullah Al Fahim
    Abdullah Al FahimNov 15, 2024

    n6

  • Appasaheb
    AppasahebNov 15, 2024

    Great very useful all points.
    Thanks for sharing ☺️

  • z2lai
    z2laiNov 16, 2024

    Nice list of features and examples. The infer concept seems helpful but I still don't understand it as there wasn't an explanation, but good enough for me to lookup more on my own. Thanks for sharing!

  • Nozibul Islam
    Nozibul IslamNov 16, 2024

    Great. tnx.

  • SuRaj KuMar
    SuRaj KuMarNov 16, 2024

    Such a Great Article......Thanks for Sharing....!!!!!!

  • eshimischi
    eshimischiNov 16, 2024

    These technics are not "advanced" because all of them just what make Typescript - Typescript. Not the first day.

    • Niharika Goulikar
      Niharika GoulikarNov 17, 2024

      Every programming language has advanced concepts that you come across as you gain more experience. These concepts are, of course, expressed in the language itself, so I'm not sure what you're trying to point out here.

  • Quân Trần
    Quân TrầnNov 18, 2024

    something new to learn about typescript <3

  • Gian Carlo Carranza
    Gian Carlo CarranzaNov 28, 2024

    Wouldn't example 3. implemented in the UserRepository class should be:

         class UserRepository implements Repository<User, number | string>
    
    Enter fullscreen mode Exit fullscreen mode

    Since you've passed number | string as type in the parameter of getById method?

    Although it will be more longer, I think it is more appropriate to stick to the pattern,
    Great article btw 😉.

    • Niharika Goulikar
      Niharika GoulikarNov 28, 2024

      getById(idOrName: number | string): User | undefined, works because it is structurally compatible with the interface. TypeScript ensures that the method can handle number as required by the interface. The additional support for string is treated as extra functionality, not a violation. TypeScript does not strictly enforce that methods must match the interface exactly, as long as they satisfy the required types. This flexibility allows the class to extend functionality while remaining compatible.

      Yes, we can adjust it as you suggested for better clarity. However, the provided example is already working perfectly fine.

  • Helbin Rapheal
    Helbin RaphealDec 1, 2024

    It would be interesting to go through some real-world examples or use cases where these advanced TypeScript concepts, like conditional types or dependency injection, have significantly improved the development process or project outcomes.

  • Mark Santiago
    Mark SantiagoDec 3, 2024

    Good article. thank you

  • Chety
    ChetyDec 4, 2024

    Examples are often too lengthy and can be confusing, particularly those related to generics. The last example, "Dependency Injection," is not specifically a TypeScript concept. I was expecting to see advanced TypeScript concepts, as the title suggests, but it feels more like clickbait.

  • АнонимDec 5, 2024

    [hidden by post author]

    • Niharika Goulikar
      Niharika GoulikarDec 5, 2024

      Thanks for your feedback.
      Some examples might be AI-generated, but I customized a few of them for better understanding. I also referred to the official documentation for accuracy.

Add comment