Om du har skrivit TypeScript ett tag men generics fortfarande känns som något du kopierar från Stack Overflow utan att fullt förstå, är den här artikeln det som fixar det. Generics är funktionen som tar TypeScript från "JavaScript med typannotationer" till ett genuint expressivt typsystem. När de klickar ser du dem överallt — och du sträcker dig efter dem naturligt istället för att defaulta till any när saker blir knepigt. Handbokens kapitel om generics i TypeScript-handboken täcker hela specifikationen; den här artikeln fokuserar på de mönster du faktiskt kommer att använda i riktiga kodbaser.

Problemet med any

Generics finns för att lösa ett specifikt problem: du vill att en funktion eller datastruktur ska fungera med flera typer, men du vill inte kasta bort typinformationen i processen. Den naiva lösningen är any — och det ser bra ut tills du inser vad du har gett upp:

ts
// With any — TypeScript has no idea what comes out
function identity(arg: any): any {
  return arg;
}

const result = identity('hello');
// result is typed as 'any' — you've lost the string type
// TypeScript won't catch this:
result.toFixed(2); // no error at compile time, crashes at runtime

Typinformationen går in, men den kommer inte ut. Vad du än gör med result är helt okontrollerat. Det är inte en TypeScript-kodbas — det är JavaScript med extra steg. Generics löser detta genom att låta dig säga: "Jag vet inte den exakta typen ännu, men vad som än går in ska komma ut med samma typ."

ts
// With a generic — T flows through the function
function identity<T>(arg: T): T {
  return arg;
}

const result = identity('hello');
// result is typed as 'string' ✅
result.toFixed(2); // ❌ TypeScript catches this: Property 'toFixed' does not exist on type 'string'

const count = identity(42);
// count is typed as 'number' ✅

<T> deklarerar en typparameter — tänk på det som en variabel för typer. TypeScript härleder vad T är från argumentet du skickar in, så du behöver nästan aldrig skriva det explicit. identity<string>('hello') fungerar, men det gör även identity('hello') — TypeScript räknar ut det.

Generiska funktioner i praktiken

Identity-funktionen är det kanoniska undervisningsexemplet men inte något du skulle skriva i produktion. Här är de typer av generiska funktioner som faktiskt dyker upp i riktiga kodbaser:

ts
// A type-safe array first/last helper
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const firstUser = first(users);       // UserProfile | undefined
const lastOrder = last(orders);       // Order | undefined
const firstNum  = first([1, 2, 3]);   // number | undefined

// Group array items by a key
function groupBy<T, K extends string | number>(
  items: T[],
  getKey: (item: T) => K
): Record<K, T[]> {
  return items.reduce((acc, item) => {
    const key = getKey(item);
    if (!acc[key]) acc[key] = [];
    acc[key].push(item);
    return acc;
  }, {} as Record<K, T[]>);
}

// TypeScript infers T as Order and K as string
const byStatus = groupBy(orders, order => order.status);
// byStatus: Record<string, Order[]>
// byStatus['pending'] is Order[] ✅

Lägg märke till hur TypeScript härleder T och K automatiskt från hur du anropar funktionen. Du får full typsäkerhet på returvärdet utan att skriva en enda explicit typannotation på anropsplatsen. Det är utdelningen.

Generiska gränssnitt och typer

Det här är där generics blir oumbärliga för vardagsarbete. Varje kodbas som pratar med ett API behöver en handfull generiska omslagstyper. Här är de du kommer att se (och skriva) hela tiden:

ts
// API response wrapper — used across every endpoint
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

// Paginated list response
interface PaginatedResult<T> {
  items: T[];
  page: number;
  pageSize: number;
  totalItems: number;
  totalPages: number;
}

// Using them with concrete types
interface UserProfile {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
}

interface Order {
  id: number;
  userId: number;
  total: number;
  status: 'pending' | 'shipped' | 'delivered';
}

// The return types are fully typed — no casting needed downstream
async function getUser(id: number): Promise<ApiResponse<UserProfile>> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

async function listOrders(page: number): Promise<PaginatedResult<Order>> {
  const res = await fetch(`/api/orders?page=${page}`);
  return res.json();
}

const response = await getUser(1);
response.data.email;        // ✅ string
response.data.avatarUrl;    // ✅ string

const result = await listOrders(1);
result.items[0].status;     // ✅ 'pending' | 'shipped' | 'delivered'
result.totalPages;          // ✅ number
När du bygger dessa typer från riktiga API-nyttolaster kan verktyget JSON to TypeScript generera de inre typerna (UserProfile, Order) automatiskt från ett exempelsvar. Du kopplar sedan in dem i dina generiska omslag.

Generiska begränsningar

Ibland vill du acceptera flera typer, men inte absolut vilken typ som helst. Generiska begränsningar låter dig specificera vad en typparameter måste ha. Syntaxen är T extends SomeType — vilket betyder "T måste vara tilldelningsbar till SomeType":

ts
// Without constraint — TypeScript doesn't know T has an id property
function findById<T>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id); // ❌ Property 'id' does not exist on type 'T'
}

// With constraint — T must have at least an id: number field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id); // ✅
}

// Works with any type that has id: number
const user  = findById(users, 42);   // UserProfile | undefined
const order = findById(orders, 101); // Order | undefined

// Another common constraint — T must be an object (excludes primitives)
function mergeDefaults<T extends object>(partial: Partial<T>, defaults: T): T {
  return { ...defaults, ...partial };
}

// T must have a name: string and email: string
function formatContact<T extends { name: string; email: string }>(contact: T): string {
  return `${contact.name} <${contact.email}>`;
}

// Works on any object with those two fields — UserProfile, Employee, whatever
formatContact({ name: 'Alice', email: '[email protected]', role: 'admin' }); // ✅

Begränsningen låser inte T till exakt { id: number } — det betyder att T måste ha åtminstone den formen. Så att skicka en fullständig UserProfile med tio fält är OK. Det här är strukturell typning i praktiken, vilket är en av TypeScripts kraftfullaste och välst dokumenterade funktioner.

keyof-begränsningen

Ett av de mest användbara generiska mönstren i TypeScript standardbibliotek och riktiga kodbaser är att kombinera generics med keyof. Det låter dig skriva funktioner som accepterar ett egenskapsnamn från en objekttyp och garanterar att returtypen matchar den egenskapen.

ts
// keyof T is the union of all keys of T as string literals
// K extends keyof T means K must be one of those keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: UserProfile = {
  id: 1,
  name: 'Alice',
  email: '[email protected]',
  avatarUrl: 'https://example.com/avatar.png'
};

const name  = getProperty(user, 'name');  // string ✅
const id    = getProperty(user, 'id');    // number ✅
const email = getProperty(user, 'email'); // string ✅

getProperty(user, 'password'); // ❌ Argument of type '"password"' is not assignable to
                               //    parameter of type 'keyof UserProfile'

// Real use case: a generic sort function
function sortBy<T>(items: T[], key: keyof T): T[] {
  return [...items].sort((a, b) => {
    const av = a[key];
    const bv = b[key];
    return av < bv ? -1 : av > bv ? 1 : 0;
  });
}

const byName = sortBy(users, 'name');   // ✅ sorted by name
const byId   = sortBy(users, 'id');     // ✅ sorted by id
sortBy(users, 'nonexistent');           // ❌ caught at compile time

Standardtypparametrar

Precis som funktionsparametrar kan ha standardvärden kan typparametrar det också. Det här är användbart när du har en generisk typ som nästan alltid används med en specifik typ, men du vill behålla flexibiliteten för de fall då den inte gör det.

ts
// Default type parameter — T defaults to string if not specified
interface Cache<T = string> {
  get(key: string): T | undefined;
  set(key: string, value: T, ttlMs?: number): void;
  delete(key: string): void;
  clear(): void;
}

// Without specifying T — uses the default (string)
declare const stringCache: Cache;
const val = stringCache.get('theme'); // string | undefined

// Specifying a different T
declare const userCache: Cache<UserProfile>;
const user2 = userCache.get('user:42'); // UserProfile | undefined

// Another useful example: an event emitter with a typed payload
interface TypedEvent<TPayload = void> {
  subscribe(handler: (payload: TPayload) => void): () => void;
  emit(payload: TPayload): void;
}

// Events with no payload — default void keeps the API clean
const appReady: TypedEvent = { /* ... */ };
appReady.emit(); // no argument needed

// Events with a payload
const userLoggedIn: TypedEvent<{ userId: number; timestamp: Date }> = { /* ... */ };
userLoggedIn.emit({ userId: 42, timestamp: new Date() });

Inbyggda generiska verktygstyper

TypeScript levereras med en uppsättning inbyggda verktygstyper som alla är implementerade med hjälp av de generics du har lärt dig ovan. Att förstå hur man använder dem är en av de mest praktiska TypeScript-färdigheter du kan ha. Här är kärnuppsättningen med verkliga exempel som visar utdatatypen:

ts
interface UserProfile {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
  passwordHash: string;
  createdAt: Date;
}

// Partial<T> — all fields become optional (great for PATCH payloads)
type UpdateUserPayload = Partial<UserProfile>;
// { id?: number; name?: string; email?: string; role?: ...; ... }

// Required<T> — all fields become required (reverse of Partial)
interface DraftConfig {
  apiUrl?: string;
  timeout?: number;
  maxRetries?: number;
}
type ResolvedConfig = Required<DraftConfig>;
// { apiUrl: string; timeout: number; maxRetries: number }

// Pick<T, K> — keep only the named fields
type UserSummary = Pick<UserProfile, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }
// Use in list views — only ship what the UI needs

// Omit<T, K> — exclude the named fields
type PublicUserProfile = Omit<UserProfile, 'passwordHash'>;
// { id: number; name: string; email: string; role: ...; createdAt: Date }
// Safe to include in API responses

// Record<K, V> — typed dictionary / map
type PermissionMap = Record<UserProfile['role'], string[]>;
// { admin: string[]; user: string[]; viewer: string[] }

const permissions: PermissionMap = {
  admin:  ['read', 'write', 'delete', 'admin'],
  user:   ['read', 'write'],
  viewer: ['read']
};

// ReturnType<T> — infer what a function returns (keeps types in sync automatically)
function buildUserSession(user: UserProfile) {
  return {
    token: crypto.randomUUID(),
    userId: user.id,
    role: user.role,
    expiresAt: new Date(Date.now() + 3_600_000)
  };
}

type UserSession = ReturnType<typeof buildUserSession>;
// { token: string; userId: number; role: 'admin' | 'user' | 'viewer'; expiresAt: Date }
// Change buildUserSession and UserSession updates automatically ✅

Om du konverterar befintligt JavaScript till TypeScript och bygger dessa typer från grunden kan verktyget JS to TypeScript hjälpa dig att få de initiala typerna på plats så du kan börja lägga verktygstyper ovanpå dem.

Ett realistiskt end-to-end-exempel: Typad API-klient

Här är mönstret jag alltid använder när jag bygger ett typad API-lager. Det kombinerar generiska funktioner, generiska gränssnitt och verktygstyper till något som ger dig end-to-end typsäkerhet från fetch-anropet hela vägen till komponenten som konsumerar data:

ts
// The response envelope — wraps every API response
interface ApiResponse<T> {
  data: T;
  meta: {
    requestId: string;
    duration: number;
  };
}

// Error envelope — what the API sends on failure
interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

// The generic fetch wrapper
async function fetchApi<T>(
  url: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  const res = await fetch(url, {
    headers: { 'Content-Type': 'application/json', ...options?.headers },
    ...options
  });

  if (!res.ok) {
    const err: ApiError = await res.json();
    throw new Error(`[${err.code}] ${err.message}`);
  }

  return res.json();
}

// Typed endpoint functions — T is set at each call site
async function getUser(id: number): Promise<UserProfile> {
  const { data } = await fetchApi<UserProfile>(`/api/users/${id}`);
  return data;
}

async function listOrders(userId: number, page = 1): Promise<PaginatedResult<Order>> {
  const { data } = await fetchApi<PaginatedResult<Order>>(
    `/api/users/${userId}/orders?page=${page}`
  );
  return data;
}

async function createOrder(
  payload: Pick<Order, 'userId' | 'total'>
): Promise<Order> {
  const { data } = await fetchApi<Order>('/api/orders', {
    method: 'POST',
    body: JSON.stringify(payload)
  });
  return data;
}

// Usage — fully typed, no casting anywhere
const user = await getUser(42);
console.log(user.email);          // string ✅

const orders = await listOrders(42);
orders.items[0].status;           // 'pending' | 'shipped' | 'delivered' ✅
orders.totalPages;                // number ✅

const newOrder = await createOrder({ userId: 42, total: 99.99 });
newOrder.id;                      // number ✅
Du kan experimentera med alla dessa mönster direkt i TypeScript Playground — klistra in koden, hovra över variabler för att se härledda typer, och försök bryta saker för att se vad kompilatorn fångar. Det är den snabbaste feedbackloopen för att lära sig generics.

Vanliga misstag

Några mönster att undvika när du är bekväm med generics:

  • Använda any inuti en generic. Om du skriver function wrap<T>(val: T): any har du besegrat syftet. Hela poängen är att typen flödar igenom — att använda any som returtyp eller inuti implementationen innebär att TypeScript inte kan spåra den längre.
  • Över-begränsa med specifika typer. Att skriva function process<T extends UserProfile> när du bara behöver T extends { id: number } är för restriktivt. Använd den minimala begränsningen som får din implementering att fungera — på det sättet förblir funktionen återanvändbar för olika typer som råkar ha samma form.
  • Sträcka sig efter generics när en union duger. Om en funktion tar antingen en string eller ett number och logiken är olika för varje, är function f(arg: string | number) med en typguard inuti renare än en generic. Generics lyser när samma logik gäller för alla typvarianter.
  • För många typparametrar. Om du skriver <T, U, V, W>, ta ett steg tillbaka. Det är vanligtvis ett tecken på att funktionen gör för mycket, eller att typerna kunde uttryckas som ett enda gränssnitt. Själva TypeScript-kompilatorns källkod är en bra referens — även komplexa verktyg behöver sällan mer än 2–3 typparametrar.

Sammanfattning

Generics är den punkt där TypeScript slutar vara "annoterat JavaScript" och börjar vara ett genuint kraftfullt typsystem. Kärnidén är enkel: fånga typen som en parameter så att den flödar genom din funktion eller gränssnitt utan att förlora information. Därifrån låter begränsningar dig begränsa vad som är acceptabelt, keyof ger dig typsäker egenskapsåtkomst, och de inbyggda verktytstyperna (Partial, Pick, Omit, Record) hanterar de mönster du annars skulle skriva för hand. Det bästa nästa steget är att öppna ett riktigt projekt, hitta en funktion där du använde any för att du inte visste ett annat sätt, och ersätta den med en generic. TypeScript-handboken om generics och verktygstypreferensen är de två sidorna värda att bokmärka. Och om du bygger gränssnitt från riktiga API-nyttolaster, kan JSON to TypeScript generera de konkreta typerna — sedan är det bara att wrappa dem i dina generiska verktygstyper som ApiResponse<T> och PaginatedResult<T> för att få fullständig end-to-end typsäkerhet.