Mastering Advanced Type Features in TypeScript

Chandrashekhar Fakirpure

Mar 23, 2025

Share:

In this blog post, we're mastering advanced type features in TypeScript. Features like Generics, Union, Intersection, Mapped Types.

TypeScript has grown to become a go-to language for large-scale web applications. One of the reasons for its popularity is how it adds powerful type safety to JavaScript, increasing both the reliability and maintainability of your code. Beyond the basic type declarations, TypeScript offers advanced type features—Generics, Unions, Intersections, and Mapped Types—that help you write more expressive and robust code. Below, we’ll explore these features, show practical examples of how to use them, and discuss how they can reduce bugs in large-scale applications.

Introduction to Generics

Generics are a way to create reusable components that can work with different data types while still preserving type safety. They let you define placeholders (like T) for types, which are then filled in by the consumer of that generic function or class.

Why They Matter:

  • Increase flexibility: Write functions or classes that can handle different types while maintaining type checks.
  • Reduce code duplication: One generic function can replace many overloaded functions or repeated logic.
  • Improve readability: Make your intentions explicit, indicating that a piece of code can work with multiple types.

Basic Example of a Generic Function:

function identity<T>(arg: T): T {
  return arg;
}

// Using the generic function
const numberValue = identity<number>(42); // T is number
const stringValue = identity<string>("Hello Generics"); // T is string

In this simple example, the type of arg is inferred based on what we pass in. TypeScript ensures that the function returns the same type that it takes as input, reducing the chance of accidental type mismatches.

Generic Constraints

Sometimes, you want your generic to work only with certain shapes of data. You can add constraints to your generics with extends.

interface HasId {
  id: number;
}

function getId<T extends HasId>(item: T): number {
  return item.id;
}

// Valid because the object passed has 'id' property
const user = { id: 101, name: "Alice" };
console.log(getId(user)); // 101

// Error: Argument of type '{ name: string; }' is not assignable 
//        to parameter of type 'HasId'.
// console.log(getId({ name: "Bob" }));

By enforcing T extends HasId, TypeScript will warn you if the object doesn’t have the required id property.

Exploring Union Types

Union types let you combine multiple types into one. A variable of a union type can be one of the specified types, which is particularly useful when data may come in different forms.

Why They Matter:

Handle multiple possible shapes of data without losing type safety.

Encourage narrowing logic: You explicitly handle each case rather than ignoring potential differences.

Union Type in Action:

function formatInput(input: string | number): string {
  if (typeof input === "number") {
    // Convert number to string
    return input.toFixed(2);
  }
  // input must be string here
  return input.trim();
}

const result1 = formatInput(123.456); // "123.46"
const result2 = formatInput("  Hello Union  "); // "Hello Union"

In this example, input can be either a string or a number. TypeScript lets you write code that treats each scenario correctly, and it provides error checks if you try to do something invalid (like calling toFixed on a string).

Intersection Types

Intersection types (&) let you combine multiple types into a single type that has the properties of all of them. This is especially handy when working with complex data structures or when you want to extend existing types with additional properties.

Why They Matter:

Compose multiple types together, ensuring the result must satisfy all constraints.

Ideal for scenarios where you merge different objects or want to create specialized variants of an existing type.

Intersection Type Example:

interface ContactDetails {
  email: string;
  phone: string;
}

interface PersonalInfo {
  name: string;
  age: number;
}

// A type that has all the properties of both interfaces
type Customer = ContactDetails & PersonalInfo;

const customer: Customer = {
  email: "customer@example.com",
  phone: "123-456-7890",
  name: "John Doe",
  age: 30,
};

The Customer type must include email, phone, name, and age. If any of these properties are missing or the types are incorrect, TypeScript will flag it as an error.

Mapped Types

Mapped Types are an incredibly powerful feature that let you transform existing types into new ones. They’re especially useful when you need to apply a transformation (like readonly or making all properties optional) to every property of an existing interface.

Why They Matter:

Dynamically manipulate type properties, saving you from writing repetitive boilerplate.

Create utility types that adapt to different interfaces or shapes, promoting code reuse.

Basic Mapped Type Syntax:

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

Here, we create a type ReadonlyPerson based on Person, marking all properties of Person as readonly.

Practical Example with Mapped Types:

interface Person {
  name: string;
  age: number;
  email: string;
}

// A mapped type that marks all properties optional
type PartialPerson = {
  [Key in keyof Person]?: Person[Key];
};

const partial: PartialPerson = {
  name: "Alice",
  // 'age' and 'email' are not required now
};

In this snippet, the mapped type PartialPerson iterates through the keys of Person and makes every property optional. If you’ve used utility types like Partial<T> from TypeScript’s standard library, you’ve already used this concept.

You can also combine multiple transformations in mapped types. For instance, marking properties as both readonly and optional:

type ReadonlyOptionalPerson = {
  readonly [Key in keyof Person]?: Person[Key];
};

Advanced Examples in Large-Scale Applications

Let’s say you’re working on a large-scale e-commerce application. It has thousands of products, each with variations in size, color, or category. You might define generic functions to handle search filters, union types for the different product categories, intersection types for user roles with additional permissions, and mapped types to dynamically create new configurations.

Generic Function for Searching

interface Product {
  id: number;
  name: string;
  category: string;
}

function searchItems<T extends { id: number }>(items: T[], query: Partial<T>): T[] {
  return items.filter((item) =>
    Object.entries(query).every(([key, value]) => item[key as keyof T] === value)
  );
}

const products: Product[] = [
  { id: 1, name: "T-Shirt", category: "Clothing" },
  { id: 2, name: "Sneakers", category: "Footwear" },
];

const results = searchItems(products, { category: "Clothing" });
// results: [{ id: 1, name: "T-Shirt", category: "Clothing" }]

This function is generic enough to search through any array of items that have an id. You don’t have to write separate search functions for different arrays of objects anymore.

Union Type for Product Categories

type ProductCategory = "Clothing" | "Footwear" | "Accessories";

function isValidCategory(category: string): category is ProductCategory {
  return ["Clothing", "Footwear", "Accessories"].includes(category);
}

// Using the type guard in the function
function setCategory(category: string): ProductCategory {
  if (!isValidCategory(category)) {
    throw new Error("Invalid product category!");
  }
  return category;
}

Here, a ProductCategory can only be one of the specified strings, ensuring you don’t assign an invalid category. The type guard function isValidCategory narrows the type from string to ProductCategory within the conditional.

Intersection Type for Admin with Additional Permissions

interface BasicUser {
  id: number;
  username: string;
}

interface AdminRights {
  canDeleteProducts: boolean;
  canBanUsers: boolean;
}

type AdminUser = BasicUser & AdminRights;

const admin: AdminUser = {
  id: 10,
  username: "superadmin",
  canDeleteProducts: true,
  canBanUsers: true,
};

This ensures that an AdminUser has both the regular user’s properties and the admin properties.

Mapped Type for Dynamic Product Configuration

interface ProductConfig {
  title: string;
  price: number;
  inStock: boolean;
}

type ProductConfigUpdate = Partial<ProductConfig>;

function updateProductConfig(
  productId: number,
  configUpdate: ProductConfigUpdate
): void {
  // Logic to update product config in the database
  console.log(`Updating product ${productId}`, configUpdate);
}

// Usage
updateProductConfig(1, { price: 24.99, inStock: false });

ProductConfigUpdate is a mapped type that allows you to partially update a product’s configuration without needing to specify all properties.

Key Takeaways

  • Generics let you write code that can adapt to different types while preserving type safety.
  • Union Types enable you to define variables that can take different shapes, enforcing the appropriate checks in each scenario.
  • Intersection Types merge multiple types into a single type that satisfies all constraints, handy for combining roles or properties.
  • Mapped Types empower you to dynamically transform existing types, reducing boilerplate and making your code more flexible and expressive.

By mastering these advanced type features, you’ll be well-equipped to tackle complex data structures and application logic. Your TypeScript code will not only be more concise and maintainable but also far less error-prone. Whether you’re building a small library or a massive enterprise application, leveraging these advanced type capabilities will significantly improve the developer experience and stability of your codebase.

Use these features thoughtfully, and you’ll find yourself writing fewer tests for trivial type issues and more time shipping features that truly matter to your users. Have fun exploring the power of TypeScript’s type system, and watch your productivity and confidence in your code soar!

Start Project

Bring your vision to life with a custom website. Let's chat about your goals.

Let's Collab

Your next departure starts here

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG