Hvis du har skrevet TypeScript et stykke tid, men generics stadig føles som noget du kopierer fra Stack Overflow uden at forstå det fuldt ud, er denne artikel løsningen. Generics er den funktion, der tager TypeScript fra "JavaScript med typeannotationer" til et genuint ekspressivt typesystem. Når det klikker, ser du dem overalt — og du vil naturligt nå efter dem frem for at defaulte til any når tingene bliver vanskelige. Kapitlet om generics i TypeScript-håndbogen dækker hele specifikationen; denne artikel fokuserer på de mønstre, du faktisk vil bruge i rigtige kodebaser.

Problemet med any

Generics eksisterer for at løse et specifikt problem: du vil have en funktion eller datastruktur til at fungere med flere typer, men du vil ikke smide typeinformationen væk i processen. Den naive løsning er any — og det ser fint ud, indtil du indser, hvad du har givet op:

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

Typeinformationen går ind, men den kommer ikke ud. Hvad du end gør med result er fuldstændig ukontrolleret. Det er ikke en TypeScript-kodebase — det er JavaScript med ekstra trin. Generics løser dette ved at lade dig sige: "Jeg kender ikke den eksakte type endnu, men hvad der end går ind, skal komme ud med den samme type."

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> deklarerer en typeparameter — tænk på det som en variabel for typer. TypeScript udleder, hvad T er fra det argument, du sender ind, så du næsten aldrig behøver at skrive det eksplicit. identity<string>('hello') virker, men det gør identity('hello') også — TypeScript finder ud af det.

Generiske funktioner i praksis

Identity-funktionen er det kanoniske undervisningseksempel, men ikke noget du ville skrive i produktion. Her er de typer generiske funktioner, der faktisk dukker op i rigtige kodebaser:

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æg mærke til, hvordan TypeScript automatisk udleder T og K fra, hvordan du kalder funktionen. Du får fuld typesikkerhed på returværdien uden at skrive en eneste eksplicit typeannotation på kaldstedet. Det er udbyttet.

Generiske grænseflader og typer

Det er her generics bliver uundværlige for dagligdagsarbejdet. Enhver kodebase, der taler med et API, ender med at have brug for en håndfuld generiske omslagstyper. Her er dem, du vil se (og skrive) hele 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 disse typer fra rigtige API-nyttelaster, kan værktøjet JSON to TypeScript automatisk generere de indre typer (UserProfile, Order) fra et eksempelsvar. Du sætter dem derefter ind i dine generiske omslag.

Generiske begrænsninger

Nogle gange vil du acceptere flere typer, men ikke absolut alle typer. Generiske begrænsninger lader dig specificere, hvad en typeparameter skal have. Syntaksen er T extends SomeType — hvilket betyder "T skal kunne tildeles til 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 ikke T til præcis { id: number } — det betyder, at T skal have mindst den form. Så at sende en fuld UserProfile med ti felter er fint. Det her er strukturel typning i aktion, som er en af TypeScripts mest kraftfulde og bedst dokumenterede funktioner.

keyof-begrænsningen

Et af de mest nyttige generiske mønstre i TypeScript standardbibliotek og rigtige kodebaser er at kombinere generics med keyof. Det lader dig skrive funktioner, der accepterer et egenskabsnavn fra en objekttype og garanterer, at returtypen matcher den egenskab.

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

Standard typeparametre

Ligesom funktionsparametre kan have standardværdier, kan typeparametre det også. Det er nyttigt, når du har en generisk type, der næsten altid bruges med en specifik type, men du vil beholde fleksibiliteten for de tilfælde, hvor den ikke gør.

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() });

Indbyggede generiske værktøjstyper

TypeScript leveres med et sæt indbyggede værktøjstyper som alle er implementeret ved hjælp af de generics, du har lært ovenfor. At forstå, hvordan man bruger dem, er en af de mest praktiske TypeScript-færdigheder, du kan have. Her er kernesættet med rigtige eksempler der viser outputtypen:

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 ✅

Hvis du konverterer eksisterende JavaScript til TypeScript og bygger disse typer fra bunden, kan værktøjet JS to TypeScript hjælpe dig med at få de initiale typer på plads, så du kan begynde at lægge værktøjstyper oven på dem.

Et realistisk end-to-end-eksempel: Typsikker API-klient

Her er det mønster, jeg altid bruger, når jeg bygger et typsikkert API-lag. Det kombinerer generiske funktioner, generiske grænseflader og værktøjstyper til noget, der giver dig end-to-end typesikkerhed fra fetch-kaldet hele vejen til komponenten der forbruger 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 eksperimentere med alle disse mønstre direkte i TypeScript Playground — indsæt koden, hover over variabler for at se udledte typer, og prøv at bryde ting for at se hvad compileren fanger. Det er den hurtigste feedbackloop til at lære generics.

Almindelige fejl

Nogle mønstre at undgå, når du er fortrolig med generics:

  • Brug af any inde i en generic. Hvis du skriver function wrap<T>(val: T): any, har du besejret formålet. Hele pointen er, at typen flyder igennem — at bruge any som returtype eller inde i implementeringen betyder, at TypeScript ikke kan spore den længere.
  • Over-begrænsning med specifikke typer. At skrive function process<T extends UserProfile> når du kun behøver T extends { id: number } er for restriktivt. Brug den minimale begrænsning der får din implementering til at fungere — på den måde forbliver funktionen genanvendelig på tværs af forskellige typer der tilfældigvis har samme form.
  • At nå efter generics når en union ville klare det. Hvis en funktion tager enten en string eller et number og logikken er forskellig for hver, er function f(arg: string | number) med en typevagt indeni renere end en generic. Generics skinner, når den samme logik gælder for alle typevarianter.
  • For mange typeparametre. Hvis du skriver <T, U, V, W>, tag et skridt tilbage. Det er normalt et tegn på, at funktionen gør for meget, eller at typerne kunne udtrykkes som en enkelt grænseflade. TypeScript-compilerens kildekode er en god reference — selv komplekse hjælpeprogrammer har sjældent brug for mere end 2–3 typeparametre.

Opsummering

Generics er det punkt, hvor TypeScript holder op med at være "annoteret JavaScript" og begynder at være et genuint kraftfuldt typesystem. Kernekonceptet er simpelt: fang typen som en parameter så den flyder gennem din funktion eller grænseflade uden at miste information. Derfra lader begrænsninger dig indsnævre det, der er acceptabelt, keyof giver dig typesikker egenskabsadgang, og de indbyggede værktøjstyper (Partial, Pick, Omit, Record) håndterer de mønstre, du ellers ville skrive i hånden. Det bedste næste skridt er at åbne et rigtigt projekt, finde en funktion, hvor du brugte any fordi du ikke vidste en anden måde, og erstatte den med en generic. TypeScript-håndbogen om generics og referencen for værktøjstyper er de to sider, der er værd at bogmærke. Og hvis du bygger grænseflader fra rigtige API-nyttelaster, kan JSON to TypeScript generere de konkrete typer — derefter er det bare et spørgsmål om at pakke dem ind i dine generiske værktøjstyper som ApiResponse<T> og PaginatedResult<T> for at få fuld end-to-end typesikkerhed.