Skip to Content
Course content

42: Generic Constraints

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a developer on my team a few years back who was trying to build a generic "Analytics Logger." The goal was simple: the logger should take any object, but it had to be an object that contained a userId property so we could track who was doing what. They started with a simple generic function: function logEvent<T>(event: T) { ... }.

The moment they tried to access event.userId inside the function, TypeScript threw a fit. It told them that userId didn't exist on type T. Of course it didn't! As far as TypeScript was concerned, T could be a string, a boolean, or a function. The developer's first instinct was to cast the event to any to make the error go away. I had to stop them right there. Casting to any is like turning off the lights because you can't find your keys; it solves the immediate frustration, but you're still in the dark. That's where generic constraints come in.

Narrowing the Scope of T

When you use a plain generic like <T>, you're telling TypeScript, "This can be literally anything." But usually, you don't actually want anything; you want something that fits a specific shape. We use the extends keyword to create a constraint. This tells TypeScript that while T can be any type, it must at least satisfy the requirements of a specific interface or type.

interface HasId {
  id: string | number;
}

function printId<T extends HasId>(item: T): void {
  // TypeScript now knows for a fact that 'id' exists on 'item'
  console.log(`The ID is: ${item.id}`);
}

const user = { id: 1, name: "Alice" };
const product = { id: "prod_123", price: 29.99 };

printId(user);    // Works great
printId(product); // Works great
printId({ name: "Bob" }); // Error: Property 'id' is missing

Notice that we aren't forcing item to be exactly a HasId object. We're saying it must extend it. This means the object can have ten other properties (like name or price), and TypeScript will still preserve those types for the rest of the function's logic while guaranteeing that id is present.

Combining Multiple Constraints

Sometimes, a single interface isn't enough. You might need a generic that satisfies two or three different requirements. You can't list multiple extends keywords one after another, but you can use an intersection type (the & operator) to combine them. I use this frequently when building plugins or middleware where an object needs to be both "configurable" and "executable."

interface Loggable {
  log(): void;
}

interface Serializable {
  toJSON(): string;
}

// T must be BOTH Loggable AND Serializable
function processData<T extends Loggable & Serializable>(item: T) {
  item.log();
  const json = item.toJSON();
  console.log(`Processed: ${json}`);
}

const myService = {
  log: () => console.log("Logging..."),
  toJSON: () => JSON.stringify({ status: "ok" }),
  version: "1.0.0"
};

processData(myService); // Valid!

By using constraints, you get the best of both worlds: the flexibility of generics and the safety of strong typing. You stop guessing what T is and start defining exactly what T is required to be.




📋 Practical Task

Build a Generic "Length-Aware" Utility

In this exercise, you need to create a utility function that can return the length of a value. However, not everything in JavaScript has a .length property (for example, numbers or booleans do not).

Your Task:

  • Define an interface called HasLength that specifies a length property as a number.
  • Write a generic function called getLength<T extends HasLength> that takes an argument of type T and returns its length.
  • Test the function with a string, an array of numbers, and an object like { length: 10, name: "Custom Object" }.
  • Verify that passing a number or a boolean to the function results in a TypeScript compilation error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.