EnriqueMark
TS

TS generics and extends

2026-02 English

The <T> generic object is probably one of the harder concepts to understand in a static language. But in essence it’s like a placeholder: you tell the compiler that some type will show up here, and it can be an existing type or one you define yourself. Anyway, there has to be a type here, and that type gets operated on and defined in whatever form I give it later. Put plainly, the declaration here is a stand-in, or a reserved word does the standing in. T doesn’t have to be written as T, anything works; but in essence it’s a “type variable” predefined at this spot, and it can stand for any type. Which gets you better hints while debugging or writing code. So generics are very useful. They let code stay type-safe and still get a huge amount of generality and flexibility.

Even though you can write whatever you want, there’s still a conventional habit, roughly these:

  • T = Type (general type)
  • K / V = Key / Value (key-value cases)
  • E = Element (collection element)
  • R = Return (return type)

On top of that, there’s the “generic constraint,” which is interesting: it can specify that the generic must have certain properties, and pin down the types of those properties. It doesn’t demand anything else, but the specified properties have to exist to pass the check. Meaning the program only lets things go on when that specified property is there.

// T can be any type, as long as it has a length property whose type is number
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");     // ✓ string has length
getLength([1, 2, 3]);   // ✓ array has length
getLength(123);         // ✗ number has no length, compile error

A extends B can be read as A has to satisfy B, or A contains B, A is a superset of B, or B is a subset of A.

If what I specify after it is a set and that set has a property in it, then in essence, since A contains that set, A must have that property too. And if what’s written there is an empty set, in JS that’s the set containing any value that isn’t null/undefined.

For example T extends {} and writing T on its own differ in nothing but null/undefined. Since everything is an object, a type is in essence a set too, and when that set has some property, it matches constraint B here, or rather the properties inside the set match, so A contains B.

Read this way, any inheritance is really building a “superset” that contains the parent class. A subclass that inherits necessarily has all the parent’s properties and methods, plus its own on top, and that’s a superset. The word “inheritance” sounds at first like the subclass should be a subset of the parent, but it’s actually the other way around. The parent class is the subset of the subclass.

They’re called parent class and subclass because you’re looking at it from the angle of “instances,” or values.

Take Animal and Dog:

  1. Every Dog is an Animal, but the other way around, an Animal isn’t necessarily a Dog, because Animal is a more abstract thing.
  2. From that angle, Dog is a subset of Animal.

But from the property angle, Dog has more properties and methods than Animal, so their relationship flips again.

  • If you divide by “level of abstraction,” Animal is the higher level and more abstract, so it’s the “bigger” one.
  • But the further you go toward the “concrete,” the more concrete methods and properties a subclass has, and in that respect it’s “bigger.” These are two different angles to look from.

As for where extends comes in, generally I only have to specify it when I need to access one of the generic’s properties inside a method, otherwise the compiler errors out because it can’t access it. But if I’m not after some property and only spread it with ..., or put plainly, iterate over all of its types, then there’s no need to specify anything.

It’s all or nothing here. If you’re going to use one specific property, you have to declare it inside. Same as declaring a variable before I use it, because a generic can be any type and the compiler doesn’t know which properties it will have, so if you don’t declare a minimum set of properties for it, something will definitely go wrong later.

// no special extends declaration needed here, since no specific property is accessed inside
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// here you have to declare it, since it accesses the id property of one of the generics, and without the declaration it errors
function merge<T extends { id: number }, U>(obj1: T, obj2: U): T & U {
  console.log(obj1.id); 
  return { ...obj1, ...obj2 };
}

The interesting bit is that the & here, from the property angle, is a union, meaning the final output type necessarily has all the properties and methods of both types; but from the value angle it’s an intersection, meaning the final output set has to belong to both type T and type U.

There’s also the vertical bar (|), which works the same as all the symbols in an if statement: & (and), | (or), not (!). The usage logic of these symbols is the same across the board. Of course, there’s no “not” logic in type checking. For generics, since a generic already means any type goes, having no not is normal. And on concrete types there’s no “not some type” either, because that would mean any type other than that concrete one satisfies the condition, so the types in that set become infinite, and ts doesn’t provide a way to declare types like that.

On the other hand, the type inside <> is more like declaring what a container holds. What it tells me is what type will be inside the returned type object.

Array<number>      // array, holding number, can also be written as number[]
Promise<Response>  // Promise, what it finally produces is Response
Map<string, User>  // Map, holding key-value entries, the key is string, the value is User

Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: TS泛型和extends.