TypeScript Generics: Functions That Work for Any Type

October 4, 2026 · 3 min read

Say you want a function that returns the first item of an array. Without generics, you have two bad options:

function firstNumber(items: number[]): number | undefined { … }
function firstString(items: string[]): string | undefined { … }
// …one per type, forever

function first(items: any[]): any { … }
// …one function, but every caller loses its types

Generics give you the third option: one function that works for every type and keeps track of which one it was given.

A type parameter

function first<T>(items: T[]): T | undefined {
  return items[0];
}

<T> declares a type parameter. It works like a function parameter, but for types: a placeholder that gets filled in each time the function is called. This signature reads as "for any type T, take an array of T and return a T or undefined."

function first<T>(items: T[]): T | undefined {
return items[0]
}
 
const n = first([1, 2, 3])
const s = first(['a', 'b'])
const u = first([{ id: 1 }])
 
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b
}
longest([1], [1, 2])
longest(10, 20)
T is
T(decided per call)
result
typeT | undefined

T is a type parameter — a placeholder for a type, the way a function parameter is a placeholder for a value. first works for an array of anything, and still knows what it returns.

0 / 5

You almost never fill in T yourself — TypeScript infers it from the arguments:

const n = first([1, 2, 3]);        // T = number   → n: number | undefined
const s = first(['a', 'b']);       // T = string   → s: string | undefined
const u = first([{ id: 1 }]);      // T = { id: number }

You can pass it explicitly when inference can't work it out, such as an empty array: first<string>([]).

Constraints: extends

Inside first, TypeScript knows nothing about T, so you can't do anything type-specific with it. When you need to, add a constraint:

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

longest([1], [1, 2]);   // T = number[]  ✓
longest('hi', 'hey');   // strings have length ✓
longest(10, 20);        // error: Argument of type 'number' is not assignable
                        //        to parameter of type '{ length: number; }'.

T extends { length: number } means "T can be any type, as long as it has a numeric length". Now a.length is allowed inside, and callers can't pass something without one.

keyof: generics that relate two parameters

Generics shine when one parameter's type depends on another's:

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: 'Ada' };
getProp(user, 'name');    // string
getProp(user, 'id');      // number
getProp(user, 'email');   // error — 'email' isn't a key of user

keyof T is the union of T's property names, and T[K] is the type of the property at key K. The return type follows whichever key you pass.

Generic types, not just functions

Types and classes take parameters too, and you already use them:

const ids: Array<number> = [1, 2];          // same as number[]
const cache = new Map<string, User>();      // keys are strings, values Users
const pending: Promise<User> = fetchUser(); // resolves to a User

And you can write your own:

type ApiResponse<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

async function getUser(id: number): Promise<ApiResponse<User>> { … }

One ApiResponse definition now describes the response for users, orders and everything else, each with the right data type.

When not to use a generic

A type parameter that appears only once isn't doing anything:

function log<T>(value: T): void { console.log(value); }   // T adds nothing
function log(value: unknown): void { console.log(value); } // clearer

The rule of thumb: a generic earns its place when it connects two things — an input to an output, or one parameter to another. If it doesn't relate anything, use a plain type.

The takeaway

Generics let you write code once and keep exact types for every caller. Read <T> as "some type, decided by the caller", extends as "but only types that have these features", and let inference do the rest.