#Generic functions
Generics in TypeScript let you write reusable code that works with many types while keeping strong type safety. A type parameter like T is a parameter for types: a function's ordinary parameters are blanks filled with values at each call, and its type parameters are blanks filled with types at each call. The checker fills them in for you (inference) or you write them explicitly (identity<number>(5)).
Why they exist: without generics you have two bad choices for a function that should "work with anything". You can write it once with any, which throws away everything the compiler knew about the argument. Or you can write one copy per type (identityString, identityNumber, …) or a stack of overloads. A generic says something more precise than either: "whatever type comes in, the same type comes out". The point of T is to connect types — between a parameter and the return type, or between two parameters.
A good mental model is a funnel with a label. The value goes in, the checker writes its type on a label called T, and every other place that mentions T in the signature gets the same label. Nothing exists at runtime: generics are erased like every other type, so identity<number>(5) compiles to plain identity(5).
function identityAny(arg: any): any {
return arg;
}
const a = identityAny("Hello"); // const a: any
a.toFixed(2); // no compile error — TypeError at runtime
function identity<T>(arg: T): T {
return arg;
}
const b = identity("Hello"); // const b: "Hello"
b.toUpperCase(); // fine
b.toFixed(2); // ❌ Property 'toFixed' does not exist on type '"Hello"'. Did you mean 'fixed'?identityAny("Hello")returnsany. The string went in, but the signature has no way to say "the same thing comes out", so the type information is gone:aisany.- On an
any, every property access is allowed, soa.toFixed(2)compiles. At runtime"Hello".toFixedisundefined, and calling it throwsTypeError: a.toFixed is not a function. identity<T>declares a type parameterTand uses it twice: for the parameter (arg: T) and for the return type (: T). That second use is what makes it useful — it links output to input.- At the call
identity("Hello"), the checker matches the argument type"Hello"against the parameter typeTand concludesT = "Hello". It substitutes that into the return type, so the call returns"Hello", andbis"Hello"(aconstkeeps the literal type). b.toUpperCase()is a string method, so it's fine.b.toFixed(2)is rejected with TS2551, because"Hello"is a string literal type and strings have notoFixed(the "Did you mean 'fixed'?" is the checker suggesting the oldString.prototype.fixedHTML method — ignore it). Same runtime code asidentityAny— the only difference is that the compiler kept track.
The doc's example shows the two ways to supply T:
function identity<T>(arg: T): T {
return arg;
}
let result1 = identity<number>(5); // explicit: T = number → let result1: number
let result2 = identity("Hello"); // inferred: T = "Hello" → let result2: string
const result3 = identity("Hello"); // inferred: T = "Hello" → const result3: "Hello"
identity<number>("5"); // ❌ Argument of type 'string' is not assignable to parameter of type 'number'.identity<number>(5)supplies the type argument explicitly. The checker doesn't infer anything — it substitutesT = numbereverywhere, so the signature becomes(arg: number) => number, checks5againstnumber(fine) and returnsnumber.identity("Hello")has no type argument, so the checker infersTfrom the argument:T = "Hello", a string literal type, and the call's type is"Hello".result2is declared withlet. A literal type that came from a literal expression is a widening literal: assigning it to a mutableletwidens it tostring, because you could reassignresult2 = "Bye"later. That's whyresult2isstring.result3is aconst, which can never be reassigned, so it keeps the literal:const result3: "Hello". This is the samelet/constwidening you get without generics (see Primitive and literal types).identity<number>("5")fixesT = numberfirst, and only then checks the argument."5"is a string, so you get TS2345 — explicit type arguments turn a generic into an ordinary, non-generic signature for that call.
When to use a generic function: whenever you'd otherwise copy the same function for each type, and the types of the inputs and the output are related. The doc's second example takes an array and returns one of its elements:
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const n = getFirstElement([10, 20, 30]); // T = number → const n: number
const s = getFirstElement(["a", "b"]); // T = string → const s: string
const nothing = getFirstElement<number>([]); // const nothing: number … but it's undefined
nothing.toFixed(2); // compiles, then throws at runtime- The parameter type is
T[]. For[10, 20, 30]the argument type isnumber[], so the checker lines upT[]withnumber[]and reads offT = number. The return typeTbecomesnumber. - For
["a", "b"]the array literal's type isstring[](array elements are widened when the array is created), soT = stringandsisstring. getFirstElement<number>([])is where the signature lies.arr[0]on an empty array isundefinedat runtime, but the return type saysT, which isnumber.- Why does the body compile? By default, indexing an array with a number gives the element type (
T) without addingundefined— TypeScript trusts that the index is in range. Soreturn arr[0]is accepted as returningT. - The result:
nothingis typednumber,nothing.toFixed(2)compiles, and at runtime it throwsTypeError: Cannot read properties of undefined (reading 'toFixed').
first returning T | undefinedThe honest version says what really comes back:
function first<T>(arr: readonly T[]): T | undefined {
return arr[0];
}
const maybe = first([10, 20, 30]); // const maybe: number | undefined
maybe.toFixed(2); // ❌ 'maybe' is possibly 'undefined'.
maybe?.toFixed(2); // fine- The return type is now
T | undefined, so withT = numberthe call returnsnumber | undefined. maybe.toFixed(2)fails with TS18048, forcing the caller to handle the empty case — which is exactly the case that crashed above.readonly T[]as the parameter type is a small extra: it promises the function won't mutate the array, and it lets callers pass both mutable arrays andreadonlyarrays oras consttuples.- If you turn on
noUncheckedIndexedAccessin tsconfig, the originalreturn arr[0]stops compiling with "Type 'T | undefined' is not assignable to type 'T'", because every index access then includesundefined. That flag is the project-wide fix for this whole class of lie.
Generics also beat overloads whenever the overloads would just be "the same signature, once per type" — see Generics are often better than overloads.