Functions and generics

Constrain a generic only when necessary

Require a capability from a type parameter while preserving the caller-specific type in the result.

A generic keeps a relationship between types the caller chooses. A constraint adds the minimum capability the function body needs to do its job. You write it with extends.

Say we want the longer of two values. The body reads .length, so both arguments must have one:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}

Strings and arrays both work, because both have a numeric length. Call it with two numbers and TypeScript stops you:

Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

Notice the return type is still T, not { length: number }. Pass two string arrays and you get a string array back, with every array method available. That is the whole point of keeping the generic instead of writing (a: { length: number }, b: { length: number }): the caller’s specific type survives the call.

A constraint is not a blank check

A constraint tells you what T has at minimum. It does not let you build any object that matches and hand it back as T:

function broken<T extends { length: number }>(value: T): T {
  return { length: value.length }
}

The object has a length, so it satisfies the constraint. But T might be a string array, and this object has no map() or join(). TypeScript rejects it:

Type '{ length: number; }' is not assignable to type 'T'.
  '{ length: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ length: number; }'.

The message is long but the meaning is short: the caller decides what T is, not you. If you find yourself building a T from scratch inside a generic function, the function probably wants a concrete return type instead.

When a generic earns its place

Use a type parameter when it relates two or more positions: two arguments, or an argument and the return value. In longest() it connects both inputs and the output.

If T appears only once, in a single parameter, a plain type is clearer. function log<T extends { length: number }>(value: T) gives you nothing that function log(value: { length: number }) does not.

Try this on your own: call longest() with two objects that also have a label property, then hover over the result. The label type should still be there.

Lesson completed