Why Does any Lose the Type You Passed In?
Type a parameter as any and TypeScript stops checking everything that comes out of the function:
| |
You know you passed an array of numbers. The compiler doesn’t, because any tells it to stop looking. n comes back as any, so calling a string method on a number compiles cleanly and throws TypeError: n.toUpperCase is not a function when it runs.
A generic names the type instead of discarding it:
| |
T is a type parameter, a placeholder that gets filled in at each call site. firstElement([1, 2, 3]) fills T with number, the return type becomes number, and the bad .toUpperCase() call fails in your editor instead of in production. You write the function once and every element type keeps its checking.
The rest of this guide is about using that without making the signature harder to read than the bug it prevents.
How Does TypeScript Infer the Type Parameter?
You didn’t write firstElement<number>([1, 2, 3]) above, and you rarely need to. TypeScript matches the argument against the parameter’s declared shape (T[]) and solves for T. It’s the same inference that types const x = 5 as a number, applied to type parameters.
With nothing to infer from, T falls back to unknown:
| |
When the call has no value to infer from, pass the type argument yourself in angle brackets.
Inference can also go wrong when two arguments feed the same T and disagree. TypeScript picks one candidate and reports the other argument as the problem, which can produce an error that names a type you never wrote:
| |
When a generic call gives you a confusing error or an unexpected type, hover the call in your editor and read what T was inferred as. That usually explains it faster than reading the message.
How Do You Constrain a Generic With extends and keyof?
An unconstrained T can be anything, so inside the function you can only do what every type supports, which is almost nothing beyond passing the value around. Most real generic functions need to assume something about T, and extends is how you say what:
| |
K extends keyof T limits K to the property names of whatever object you pass, and T[K] reads off that property’s type. The list of valid keys comes from the object at the call site, not from a hardcoded union. A typo like "pay" is a compile error instead of a silent undefined.
The constraint can also be a plain shape:
| |
Strings and arrays both have a length, so both pass. A number doesn’t, and the call is rejected before the function body ever runs. Because the return type is T and not { length: number }, logLength("hello") still hands you back a string.
How Do Generic Interfaces and Classes Work?
A generic interface is a shape with a type parameter:
| |
A generic class shares its type parameter across every field and method:
| |
new Stack<number>() and new Stack<string>() are both fully typed, and the class exists once. The same pattern shows up wherever you use dependency injection: you write a Repository<T> once, then inject Repository<User> and Repository<Order> where they’re needed instead of maintaining a near-identical class per entity.
Multiple type parameters work the same way, and each can carry its own constraint:
| |
Two parameters are also how a Result<T, E> type carries a success value and an error type side by side. If you’re deciding whether functions should throw or return failures as data, exceptions vs error values walks through that choice.
How Do Default Type Parameters Work?
A type parameter can have a default, like a function parameter with a default value:
| |
Inference still wins when there’s an argument to infer from, as raw shows. The default applies when nothing fills T, for example when you write ApiResponse as a type annotation with no type argument. That makes defaults useful on wrappers like response types and event payloads, where many call sites don’t care about the exact data type. Default to unknown, not any: it still forces a check before anyone reads data.
When Should You Use a Union Type Instead of a Generic?
Ask one question before adding <T>: does the return type depend on what was passed in? If the function returns the same type whatever the input, a union is enough and a generic adds nothing:
| |
firstElement, getProperty and Stack<T> are different. Each returns something whose type is decided by what came in, and that link between input and output is what a generic buys you. If you can write the function with a union and lose nothing, use the union.
Stacking type parameters because a function could be generic costs readability and buys nothing. A helper with three type parameters that is only ever called with string and number is harder to read than two overloads. Add <T> when a real caller needs a type to flow through, and delete it when every call site pins the same concrete type.
Why Can’t You Use T at Runtime?
Type parameters are erased at compile time. The JavaScript that runs has no T in it, so you can’t construct one, check instanceof T, or branch on it:
| |
If a function needs to build a T or inspect it at runtime, pass a real value as an argument: a constructor, a schema, or a type tag. Generics are a compile-time contract, not a runtime guard, so data coming from outside your program (an API response, JSON.parse) still needs validating even when it’s typed ApiResponse<User>.
To see every error in this guide for yourself, install TypeScript with whichever of npm, Yarn or pnpm your project uses, paste the examples into a .ts file, and run npx tsc --noEmit --strict. Then open the most-used any in your own codebase and check whether a <T> would carry the type through.