Skip to main content
Go back

TypeScript #9: Intermediate Generics (Constraints)

#typescript #generics #constraints

How to tame Generics so they don't accept "anything". Constraints, default values and the keyof trick.

In the previous part we saw that <T> can be anything. Sometimes, that is too flexible. What if we want T to have a .length property yes or yes?

Constraints (extends)

Imagine this function:

typescript
function getLength<T>(item: T) {
  return item.length; // ❌ Error: Property 'length' does not exist on type 'T'.
}

// TS complains because T could be a number (which doesn't have .length), 
// or a boolean...

To fix it, we use extends to put a restriction (Constraint).

typescript
// We say: "T can be whatever you want, BUT it must extend (fulfill) this shape"
interface HasLength {
  length: number;
}

function getLength<T extends HasLength>(item: T) {
  return item.length; // ✅ Now TS knows it exists
}

getLength("Hello"); // ✅ string has .length
getLength([1, 2, 3]); // ✅ array has .length
getLength(123); // ❌ Error: number does not have .length

Generics with keyof (The master trick)

We will see this pattern everywhere. We want a function that gets a property from an object safely.

We need to define two Generics:

  1. T: The object.
  2. K: The key. But K cannot be any string, it must be a key of T.
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const developer = {
  name: "AliceDev",
  language: "TypeScript"
};

getProperty(developer, "name"); // ✅ TS knows this returns string
getProperty(developer, "age"); // ❌ Error: "age" is not a key of developer

Default Values (=)

Just like in normal functions (function sum(a, b = 0)), Generics can have default values.

This is very useful to not force the user to write the type always.

typescript
// If we don't pass T, I'll assume it's 'string'
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

// Usage without arguments:
const simple: ApiResponse = { data: "Hello", status: 200 }; // T is string

// Usage with arguments:
const complex: ApiResponse<number> = { data: 123, status: 200 }; // T is number

Let’s try it!

Let’s create a function merge that takes two objects obj1 and obj2 and combines them. Let’s type the function using two generics T and U.