Capítulo 7 de 36

Chapter 7: Generics

Core Idea

Generics let a function, interface, or class work over a range of types while preserving exact type information end-to-end — the precision of a hand-written per-type version without the boilerplate, and without the information loss of falling back to any.

Key Concepts

  • Why not any: an any-typed identity function accepts anything but also loses what was passed in — the caller gets back any, not "whatever type I gave it." A type parameter (function identity<Type>(arg: Type): Type) captures the actual argument type and threads it through to the return type.
  • Calling a generic: explicitly (identity<string>("x")) or, far more commonly, via type argument inference — the compiler infers Type from the argument. Fall back to explicit type arguments when inference can't resolve it (common in more complex, multi-parameter generics).
  • The compiler holds you to the constraint you actually wrote: inside function loggingIdentity<Type>(arg: Type), you cannot access arg.lengthType could be anything, including number. Either narrow the parameter to Type[]/Array<Type> (arrays always have .length) or add an explicit constraint (see below).
  • Generic function types: the type of a generic function repeats the type parameter — let myIdentity: <Type>(arg: Type) => Type = identity. The parameter name in the type doesn't have to match the implementation's, only its position/arity does.
  • Generic interfaces: a call signature can be generic inside an interface (interface GenericIdentityFn { <Type>(arg: Type): Type }) — the type parameter is chosen fresh per call — or the type parameter can be hoisted to the interface itself (interface GenericIdentityFn<Type> { (arg: Type): Type }), which requires specifying it when the interface is used (GenericIdentityFn<number>) but makes it visible to every member.
  • Generic classes: class Box<Type> { ... }, parameterized the same way as interfaces. Generics apply only to the instance side of a class, never the static side — static members can't reference the class's type parameter. (Generic enums and namespaces don't exist at all.)
  • Generic constraints (<Type extends Lengthwise>): restrict a type parameter to types that satisfy a shape, unlocking property access the unconstrained version would reject. A constrained generic no longer accepts every type — only ones matching the constraint (loggingIdentity(3) fails since number has no .length; an object literal with a length property works).
  • Constraining one type parameter by another: function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) — ties Key to the actual keys of Type, so getProperty(x, "m") for a key that doesn't exist on x is a compile error instead of a runtime undefined.
  • Using class types in generics (factory pattern): to accept "a class, not an instance" as a parameter, type it by its constructor signature: function create<Type>(c: { new (): Type }): Type. A more advanced version constrains the factory's return type by extending a base class (<A extends Animal>(c: new () => A): A) — this constructor-signature pattern underlies the mixins design pattern.
  • Generic parameter defaults (<T extends HTMLElement = HTMLDivElement>): makes a type parameter optional to specify explicitly, collapsing what would otherwise require several overloads into one signature. Rules: a parameter with a default is optional, and required parameters can't follow optional ones; a default must itself satisfy that parameter's constraint if one exists; when inference can't pick a candidate, the default is used instead.
  • Variance annotations (in T / out T / in out T) — an advanced, rarely-needed escape hatch. TypeScript automatically infers covariance/contravariance for generic types structurally (a Producer<Cat> fits where Producer<Animal> is expected because Producer only produces T; a Consumer<Animal> fits where Consumer<Cat> is expected because anything that can consume any Animal can consume a Cat). Manual variance annotations exist only to correct the rare case where TS's structural inference gets it wrong (certain circular types) or as a speed optimization on extremely complex types validated by profiling — and they only affect instantiation-based comparisons, never plain structural comparisons, so they can't be used to force stricter behavior than the type structurally has.

Code Examples

interface Lengthwise {
  length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // OK — constraint guarantees .length exists
  return arg;
}
loggingIdentity({ length: 10, value: 3 }); // OK
loggingIdentity(3); // Error — number has no .length
  • What it demonstrates: a generic constraint unlocking safe property access while still preserving the caller's exact input type on return.
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
const x = { a: 1, b: 2 };
getProperty(x, "a"); // OK, inferred number
getProperty(x, "m"); // Error — "m" isn't a key of x
  • What it demonstrates: constraining one type parameter (Key) by another (keyof Type) to make invalid property access a compile error.

Reference Tables

PatternSyntaxUse when
Unconstrained generic<Type>(arg: Type)works over literally any type
Array-specific<Type>(arg: Type[])need .length/array methods, works for any element type
Constrained<Type extends Shape>(arg: Type)need specific members, still preserve exact input type
Cross-parameter constraint<T, K extends keyof T>one parameter's valid values depend on another
Constructor/factory<Type>(c: { new (): Type })parameter is a class, not an instance
Default type argument<T = Default>make the type argument optional to specify

Key Takeaways

  1. Reach for a type parameter (not any) whenever a function's output type genuinely depends on its input type — that's the entire point of generics.
  2. Constrain generics to the minimum shape you actually need (extends Lengthwise, not a concrete type) to keep them reusable while still safe to operate on.
  3. Static class members can never use the class's own type parameter — design factories/statics around that limitation.
  4. Don't write variance annotations unless you've specifically diagnosed a structural-inference bug or a profiled performance issue — TypeScript infers variance correctly in the overwhelming majority of cases, and a wrong annotation causes unpredictable behavior.

Connects To

  • More on Functions: the "push type parameters down, use fewer type parameters, each parameter should appear twice" guidelines apply directly to everything in this chapter.
  • Object Types: Box<Type> and other generic object/interface types build on the same mechanics.
  • Keyof/Typeof, Mapped Types, Conditional Types: the operators most commonly combined with generics to build reusable utility types.
  • Classes: static-vs-instance side distinction referenced here is covered in full in the Classes chapter.