Generic Functions
What goes in as T, comes out as T. No surprises.
Generic functions are functions that work with any type while preserving type information. The function doesn't know what T is when you write it — it knows when you call it. This is the core idea that makes generics so powerful. Without generics, you'd either write separate functions for each type (repetition!) or use any (losing type safety!). Generic functions give you a third option: write the function once, and the type is determined at the call site.
Think of the Hyderabadi auto. An auto doesn't care WHO sits inside — student, IT employee, tourist, Nizam's descendant — everyone gets transported from point A to point B. The auto is the generic function. The passenger type is T. When a student sits in, the auto becomes Auto<Student>. When a tourist sits in, it's Auto<Tourist>. The transport logic is the same, but the passenger type changes each ride.
And here's the key: the auto RETURNS the same type that went in. You don't board as a student and arrive as a biryani! What goes in as T, comes out as T. That's the contract of generic functions — the type flows through from input to output, and TypeScript enforces this contract at compile time. This is what separates generics from any: with any, you lose the type; with generics, you keep it.
There are three ways to write generic functions in TypeScript. Each has the same effect — the choice depends on your coding style and the context in which you're working. Function declarations are the most common and readable. Arrow expressions are preferred in functional programming styles and when passing functions as callbacks. Full type annotations are useful when you want to separate the type signature from the implementation.
1. Function Declaration:
function identity<T>(value: T): T {
return value;
}
2. Arrow Function Expression:
const identity = <T>(value: T): T =>
value;
3. Full Type Annotation:
const identity: <T>(value: T) => T =
(value) => value;
Calling Generic Functions: You can call them two ways. Explicit — you tell TypeScript what T is: identity<string>("hello"). Inferred — TypeScript figures it out from the argument: identity("hello"). Both work perfectly! TypeScript is smart enough to look at the argument and determine the type. Most of the time, inference is what you'll use — it's cleaner and less verbose. Explicit type arguments are for when inference can't figure things out on its own.
Practical Examples:
function first<T>(
arr: T[]
): T | undefined {
return arr[0];
}
function last<T>(
arr: T[]
): T | undefined {
return arr[arr.length - 1];
}
function wrap<T>(value: T): T[] {
return [value];
}
Notice how T tracks through the entire function — if you pass number, T is number everywhere: the parameter type, the return type, and inside the function body. TypeScript ensures you don't accidentally return a different type! If the parameter is T and the return type is T, you must return the same type. This is what makes generic functions type-safe — no cheating, no surprises. The compiler has your back.
Sometimes one type parameter isn't enough. Your function might need to handle two or more independent types. TypeScript lets you declare multiple type parameters inside the angle brackets: <T, U>. Each type parameter is a separate "slot" that gets filled independently when the function is called. The first argument determines T, the second determines U, and so on.
A Pair Function:
function pair<T, U>(
first: T,
second: U
): [T, U] {
return [first, second];
}
Calling: pair("hello", 42) — TypeScript infers T = string, U = number, and the return type is [string, number]. Each type parameter is independent! They don't have to be different — pair("a", "b") gives T = string, U = string — but they can be different, and that's the whole point.
A Map Function:
function map<T, U>(
arr: T[],
fn: (item: T) => U
): U[] {
return arr.map(fn);
}
This transforms each element from type T to type U — like the RTC bus route that picks up passengers from different stops and drops them at different destinations. The input type and output type can be different! The callback function (item: T) => U is what does the conversion. You give it a T, it gives you back a U.
A Merge Function:
function merge<T, U>(
a: T,
b: U
): T & U {
return { ...a, ...b };
}
This combines two objects of different types into one. The intersection type T & U means the result has all properties of both T and U. Like mixing Irani chai and Osmania biscuits — both maintain their identity but come together on the same plate!
Three Type Parameters: You can use <T, U, V> — rare but valid when the function genuinely needs three independent types. For example, a function that takes a key of type T, a value of type U, and a validator of type V.
Naming Convention: Single letters for simple cases (T, U, V). Descriptive names for complex cases: TInput, TResult, TKey, TValue. The T prefix signals "this is a type parameter, not a concrete type." This convention helps other developers immediately recognize generics in your code.
TypeScript's type inference is powerful, but sometimes you need to take control. Knowing when to rely on inference and when to specify explicitly is a key skill that separates intermediate from advanced TypeScript developers. Let's break it down clearly.
When Inference Works Great:
// TS infers T = string
identity("hello");
// TS infers T = number
first([1, 2, 3]);
When the type can be determined from the arguments, inference is your friend. The result is used immediately and TypeScript can propagate the type downstream to whatever variable receives the return value. This is the happy path — let the compiler do the work and keep your code clean.
When You NEED Explicit Types:
1. No arguments to infer from:
function createBox<T>(): {
value: T;
} {
return {
value: undefined as any
};
}
// Can't infer T — no args!
createBox(); // T = unknown
// Must specify explicitly
createBox<string>(); // T = string
When a generic function has no parameters from which to infer T, TypeScript is stuck. You must specify the type argument explicitly. This pattern is common in factory functions and builders.
2. Inference gives a union but you want specific:
// Infers T = string | number
first([1, "two", 3]);
// Force a narrower type
first<number>([1, "two", 3]);
3. You want a narrower or different type:
identity<string | number>("hello");
The .tsx Gotcha: In React TSX files, <T> looks like a JSX tag to the parser! This causes a syntax error. The fix is simple but non-obvious:
// WRONG in .tsx files
const fn = <T>(x: T) => x;
// FIX 1: trailing comma
const fn = <T,>(x: T) => x;
// FIX 2: extend constraint
const fn = <T extends {}>(x: T) =>
x;
The trailing comma <T,> is the most common fix — it tells TypeScript "this is a generic, not JSX." Remember this one — it'll save you hours of debugging when you start writing generic React components!
Here's your complete cheatsheet for generic functions. Pin this to your mental whiteboard and refer back whenever you need it!
Declaration Syntax:
function fn<T>(param: T): T {
return param;
}
Arrow Syntax:
const fn = <T>(param: T): T =>
param;
Multiple Type Parameters:
function fn<T, U>(
a: T,
b: U
): [T, U] {
return [a, b];
}
Inference: fn("hello") — T is inferred as string. Let TypeScript do the work when it can. This keeps your call sites clean and readable.
Explicit: fn<string>("hello") — you specify the type. Use this when inference can't figure it out, like factory functions with no arguments, or when you want a narrower type than what's inferred.
Naming Convention:
T,U,V— simple cases with few type paramsTKey,TValue— when types have specific rolesTInput,TResult— when clarity matters more than brevity
TSX Gotcha: Use <T,> or <T extends {}> in .tsx files to avoid JSX ambiguity.
Key Rules:
- T preserves type information end-to-end — what goes in as T comes out as T
- Multiple type params for independent types that don't depend on each other
- Let TypeScript infer when possible, specify explicitly when needed
- Generic functions are the most common and practical use of generics
The Golden Rule: Generic functions are the Hyderabadi auto — same ride logic, any passenger type. What gets in as T comes out as T. No surprises, bhai!
Key Points
- Generic functions work with any type while preserving type information — T flows from input to output
- Three syntaxes: function declaration, arrow expression, full type annotation
- Multiple type parameters
for independent types — like different passengers in the same auto - Let TypeScript infer when possible; specify explicitly when it can't infer
- In .tsx files, use
or to avoid JSX ambiguity
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login