TypeScript har et rigt typesystem, men du behøver ikke forstå det hele for at være produktiv. Virkeligheden er, at 90% af arbejdet klares af en håndfuld funktioner, du bruger hver dag. Dette er den praktiske reference — primitiver, unions, generics, utility types og de indsnævringsmønstre, der får det hele til at klikke. Ingen abstrakt teori; hvert eksempel er den slags kode, du skriver i et rigtigt projekt. Hvis du kender JavaScript godt og har lavet noget TypeScript, men vil styrke din mentale model, er dette til dig. Den fulde historie er i TypeScript-håndbogen — denne artikel er 80/20-versionen.

Primitiver og literaler

TypeScripts primitive typer mapper direkte til JavaScripts runtime-typer: string, number, boolean, null, undefined, bigint og symbol. Du bruger de første tre konstant; resten optræder i specifikke kontekster.

ts
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null;     // nullable pattern
let refreshToken: string | undefined;  // optional pattern

Hvor TypeScript bliver interessant er literaltyper. I stedet for blot at sige, at en værdi er en string, kan du sige, at den skal være en af et specifikt sæt strenge. Dette er langt mere nyttigt end en simpel string-annotation, fordi TypeScript kan fange ugyldige værdier på kompileringstidspunktet:

ts
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function navigate(dir: Direction) {
  console.log(`Moving ${dir}`);
}

navigate('north');  // ✅
navigate('up');     // ❌ Argument of type '"up"' is not assignable to type 'Direction'

// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});
(string & {})-tricket til sidst holder autofuldførelse i gang for de kendte værdier, mens det stadig accepterer enhver streng. Det er et almindeligt mønster i designsystembiblioteker.

interface vs type — hvornår bruger man hvad

Dette er det spørgsmål, enhver TypeScript-begynder stiller. Det praktiske svar: brug interface til objektformer — især dem, der repræsenterer offentlige API-kontrakter eller som andre typer vil udvide. Brug type til unions, skæringer, mappede typer og alt, der ikke udelukkende er en objektform. I praksis er de stort set udskiftelige for objektformer — vælg én konvention og vær konsekvent inden for en codebase.

ts
// interface — object shapes, extensible via extends
interface User {
  id: number;
  email: string;
  createdAt: Date;
}

interface AdminUser extends User {
  role: 'admin';
  permissions: string[];
}

// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;

// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
  createdAt: Date;
  updatedAt: Date;
};

type UserRecord = User & WithTimestamps;

// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; code: number };

En meningsfuld forskel: interfaces understøtter deklarationssammenfletning — du kan erklære den samme interface to gange, og TypeScript fletter definitionerne. Sådan udvider biblioteker globale typer (f.eks. tilføjelse af egenskaber til Window). Types fletter ikke; at generklære en type er en fejl.

Union- og skæringstyper

Uniontyper (A | B) siger "denne værdi er enten A eller B". Skæringstyper (A & B) siger "denne værdi er både A og B på samme tid". Unions er overalt i rigtig kode — det mest kraftfulde mønster, de muliggør, er diskrimineret union, som er, hvordan du modellerer data, der kan være i forskellige tilstande, uden at ty til valgfrie felter overalt.

ts
// Discriminated union — model API response states cleanly
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; retryable: boolean };

// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':    return '<p>Not started</p>';
    case 'loading': return '<p>Loading...</p>';
    case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
    case 'error':   return `<p>Error: ${state.error}</p>`;
  }
}

// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
  label: string;
  disabled?: boolean;
};

type IconButtonProps = ButtonBaseProps & {
  icon: string;
  iconPosition: 'left' | 'right';
};

Det diskriminerede unionsmønster er baseret på en diskriminant — en literal egenskab (her status), der er unik for hver variant. TypeScript bruger den egenskab til at indsnævre den fulde type inde i betingede grene, hvilket giver dig typesikker adgang til variantspecifikke felter som data eller error.

Generics — den del, der snubler alle

Det motiverende problem: du vil have en funktion, der arbejder på flere typer, men brug af any kasserer al typeinformation. Generics løser dette — de er typeparametre, skrevet i vinkelbuer, der lader en funktion eller interface arbejde over en familie af typer, mens fuld typesikkerhed bevares.

ts
// Without generics — any loses all information
function first(arr: any[]): any {
  return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here

// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);       // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅

Syntaksen <T> erklærer en typeparameter. Du kan navngive den hvad som helst — T er bare konventionen for en enkelt generisk type. Når du kalder funktionen, udleder TypeScript T fra argumenterne, så du sjældent behøver at skrive det eksplicit. Generiske interfaces er lige så nyttige til modellering af API-former:

ts
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
  data: T;
  meta: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
}

interface User {
  id: number;
  name: string;
  email: string;
}

// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const res = await fetch('/api/users');
  return res.json();
}

const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number

// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const user = findById(users, 42); // T inferred as User
Hvornår man bruger generics: når du finder dig selv i at skrive den samme funktionssignatur to gange med forskellige typer, eller caster med as for at få den returtype du ønsker. Det er de to tegn på, at en generic ville være mere klar.

Utility types du faktisk vil bruge

TypeScript leverer et sæt af indbyggede utility types, der transformerer eksisterende typer til nye. De eliminerer behovet for manuelt at duplikere eller tilpasse typedefinitioner. Her er dem, der dukker op konstant i rigtige codebases:

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

// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

async function updateUser(id: number, payload: Partial<User>) {
  return fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(payload)
  });
}

// Required<T> — all properties become required (opposite of Partial)
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }

// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields

// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses

// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property

// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };

// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;

// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
  return {
    token: crypto.randomUUID(),
    userId,
    expiresAt: new Date(Date.now() + 86_400_000)
  };
}

type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in sync

unknown vs any vs never

Disse tre typer forvirrer de fleste udviklere, indtil de forstår, hvilket problem hver løser. any er flugtvejen — den deaktiverer typekontrol fuldstændigt for den værdi. Det er nyttigt, når man migrerer JavaScript til TypeScript, men overforbrug ødelægger formålet med TypeScript. unknown er det typesikre alternativ: værdien kan være hvad som helst, men du skal indsnævre den, inden du kan gøre noget med den. never er en type, der aldrig kan forekomme — bunden af typehierarkiet.

ts
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
  return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}

// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
  if (typeof input === 'string') {
    return input.toUpperCase(); // ✅ narrowed to string inside this block
  }
  if (typeof input === 'number') {
    return input.toFixed(2);    // ✅ narrowed to number
  }
  throw new Error(`Cannot transform value of type ${typeof input}`);
}

// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'apiUrl' in raw &&
    typeof (raw as any).apiUrl === 'string'
  ) {
    return raw as ResolvedConfig;
  }
  throw new Error('Invalid config format');
}

// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:
      // If you add a new Shape variant and forget to handle it here,
      // TypeScript will error: Type 'NewShape' is not assignable to type 'never'
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

Typeindsnævring i praksis

Typeindsnævring er, hvordan TypeScript forfiner en bred type (string | number, unknown, en diskrimineret union) til en specifik type inde i en betinget blok. TypeScript-indsnævrings-dokumentationen dækker hver guard i dybden — her er de mønstre, du skriver dagligt.

ts
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
  if (typeof val === 'string')  return val.trim();
  if (typeof val === 'number')  return val.toLocaleString();
  return val ? 'Yes' : 'No';
  // TypeScript knows val must be boolean here
}

// instanceof — class/object narrowing
function handleError(err: unknown): string {
  if (err instanceof Error)   return err.message;
  if (typeof err === 'string') return err;
  return 'An unknown error occurred';
}

// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };

function makeNoise(animal: Cat | Dog) {
  if ('meow' in animal) {
    animal.meow(); // TypeScript narrows to Cat
  } else {
    animal.bark(); // TypeScript narrows to Dog
  }
}

// Discriminated union narrowing (most common in component state)
type LoadState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

function renderState<T>(state: LoadState<T>) {
  if (state.status === 'success') {
    console.log(state.data);    // ✅ data is accessible
  } else if (state.status === 'error') {
    console.error(state.message); // ✅ message is accessible
  }
}

// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj
  );
}

// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
  if (isUser(payload)) {
    console.log(payload.email); // ✅ fully typed
  }
}

typeof-operatoren er det mest grundlæggende indsnævringsværktøj. Kombineret med instanceof, in-operatoren og diskriminantegenskabskontroller, kan du håndtere næsten ethvert indsnævringsscenarie uden at ty til any eller usikre casts.

Opsummering

TypeScripts typesystem belønner det at lære fundamentet godt frem for at memorere obskure funktioner. Literaltyper og diskriminerede unions eliminerer hele kategorier af kørselsfejl. Generics lader dig skrive genanvendelige, typesikre abstraktioner. Utility types som Partial, Pick, Omit og ReturnType holder dine typer DRY. Og unknown med indsnævring giver dig sikkerheden af typer selv ved kanterne, hvor data kommer ind fra omverdenen. Den bedste måde at befæste alt dette på er at eksperimentere interaktivt — TypeScript Playground lader dig indsætte et vilkårligt kodestykke og se de udledte typer øjeblikkeligt, ingen opsætning krævet. Når du arbejder med JSON-data i TypeScript-projekter, kan JSON Formatter og JSON til TypeScript-værktøjerne på dette websted generere interfacedefinitioner fra rigtige API-payloads automatisk.