← Guider för utvecklare
TypeScript10 min läsning

TypeScript i App Router

TypeScript-setup och praktiska mönster i Next.js App Router: typa route params, Server Actions, metadata och miljövariabler.

Next.js har förstklassigt stöd för TypeScript inbyggt. Typerna som kommer med Next.js täcker allt från route params och metadata till Server Actions och miljövariabler. Det finns ingen mellanlösning att konfigurera, inget separat paket att installera utöver de vanliga @types-paketen, och ingen magi. Den här guiden visar hur du sätter upp TypeScript korrekt och vilka mönster som faktiskt gör skillnad i en App Router-kodbas.

Sätta upp TypeScript

Skapar du ett nytt projekt är det enklast att välja TypeScript direkt i CLI:

npx create-next-app@latest --typescript

Har du ett befintligt JavaScript-projekt installerar du beroendena som devDependencies:

npm install -D typescript @types/react @types/node

Sedan räcker det med att starta utvecklingsservern:

npx next dev

Next.js upptäcker att TypeScript-paketet finns och genererar en tsconfig.json automatiskt. Du behöver inte skriva filen för hand.

Aktivera strict

Det viktigaste konfigurationssteget är att slå på strict: true i tsconfig.json. Det är av som standard i den genererade filen, men det bör du ändra direkt:

{
  "compilerOptions": {
    "strict": true
  }
}

strict är ett paraplyalternativ som aktiverar flera flaggor på en gång. De tre som gör störst praktisk skillnad är:

strictNullChecks förhindrar att du av misstag behandlar ett värde som aldrig kan vara null eller undefined när det faktiskt kan vara det. Utan den här flaggan kompilerar user.namn.toUpperCase() utan problem, även om user kan vara null. Med den måste du hantera fallet explicit.

noImplicitAny kräver att alla variabler och parametrar har en explicit typ när TypeScript inte kan härleda den på egen hand. Det förhindrar att parametrar tyst får typen any, vilket i praktiken stänger av typkontrollen för den kodsökvägen.

strictFunctionTypes kontrollerar att funktionstyper är kompatibla i rätt riktning. Det låter tekniskt, men det fångar verkliga buggar när du skickar callbacks som tar en subtyp av den förväntade typen.

De här felen är inte teoretiska kantfall, de är precis de kategorier av buggar som dyker upp i produktion när TypeScript är konfigurerat slarvigt.

Typa route params och searchParams

I Next.js 15 är params och searchParams Promises, inte vanliga objekt som de var i version 14 och tidigare. Det beror på att Next.js numera kan lösa upp dem asynkront, och typdefinitionerna återspeglar det.

Rätt mönster för en sida ser ut så här:

type Props = {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

export default async function Sida({ params, searchParams }: Props) {
  const { slug } = await params;
  const { sida } = await searchParams;
  return <h1>{slug}</h1>;
}

export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  return { title: slug };
}

Samma Props-typ används för både sidkomponenten och generateMetadata, så du deklarerar den en gång och återanvänder den.

Om projektet körs på Next.js 14 eller äldre behöver du inte awaitparams eller searchParams. De är då vanliga synkrona objekt och typerna definieras utan Promise<...>:

type Props = {
  params: { slug: string };
};

export default function Sida({ params }: Props) {
  return <h1>{params.slug}</h1>;
}

Kontrollera vilken version projektet kör i package.json om du är osäker.

Server Components och typer

Asynkrona Server Components är vanliga async-funktioner som returnerar JSX. TypeScript hanterar dem precis som vilken asynkron funktion som helst, vilket betyder att du kan typa dina hämtningsfunktioner separat och låta typinferensen flöda därifrån.

type Produkt = {
  id: string;
  namn: string;
  pris: number;
};

async function hamtaProdukter(): Promise<Produkt[]> {
  const res = await fetch("https://api.exempel.se/produkter");
  return res.json();
}

export default async function ProduktLista() {
  const produkter = await hamtaProdukter();
  return (
    <ul>
      {produkter.map((p) => (
        <li key={p.id}>
          {p.namn} - {p.pris} kr
        </li>
      ))}
    </ul>
  );
}

Notera att res.json() returnerar any. TypeScript klagar inte, men det innebär att typsäkerheten tar slut vid API-gränsen. I en klientkomponent brukar det betyda att en felaktig dataform resulterar i ett synligt renderingsfel. I en Server Component är situationen värre: koden körs på servern utan klientens error boundary, och ett typfel kan krascha hela sidan utan en begriplig felrapport.

Lösningen är att validera API-svaret vid gränsen. Zod är det vanligaste valet och passar bra ihop med TypeScript eftersom du kan härleda typen direkt från schemat. Det visas längre ned under "Typa API-svar".

Server Actions

Typa en Server Actions returvärde som en diskriminerad union. Det gör det enkelt att hantera både fel och framgång på klientsidan utan att kontrollera värden manuellt.

// actions.ts
"use server";

type ActionState = { fel: string } | { lyckades: true } | null;

export async function sparaKommentar(
  prevState: ActionState,
  formData: FormData,
): Promise<ActionState> {
  const text = formData.get("text");
  if (!text || typeof text !== "string") {
    return { fel: "Kommentaren får inte vara tom." };
  }
  await lagKommentar(text);
  return { lyckades: true };
}

På klientsidan tar useActionState actionen och det initiala tillståndet som argument. Typerna härleds automatiskt från actionen, så state har typen ActionState utan att du behöver ange den explicit:

// KommentarsFormular.tsx
"use client";
import { useActionState } from "react";
import { sparaKommentar } from "./actions";

export default function KommentarsFormular() {
  const [state, action, isPending] = useActionState(sparaKommentar, null);
  return (
    <form action={action}>
      {state && "fel" in state && <p>{state.fel}</p>}
      <textarea name="text" required />
      <button disabled={isPending}>Skicka</button>
    </form>
  );
}

Kontrollsatsen 'fel' in state fungerar som en typguard. Inuti blocket vet TypeScript att state har formen { fel: string } och inte den andra varianten. Det är diskriminerade unioner i praktiken.

Metadata-typer

Next.js exporterar typen Metadata från paketet next. Importera den för att få autokomplettering och felkontroll när du definierar metadata:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Startsida",
  description: "Välkommen till vår sajt.",
};

För dynamisk metadata returnerar du samma typ från generateMetadata:

import type { Metadata } from "next";

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  return {
    title: `${slug} - Vår sajt`,
  };
}

Metadata-typen täcker alla fält som Next.js känner till, inklusive Open Graph, Twitter Cards, robots-direktiv och kanoniska URL:er. Om du skriver ett fältnamn fel eller anger fel typ markerar TypeScript det direkt.

Vanliga mönster

Typa miljövariabler

process.env är som standard string | undefined för alla nycklar, vilket tvingar dig att nollkontrollera varje gång. Ett enklare alternativ är att utöka NodeJS.ProcessEnv med de variabler ditt projekt faktiskt använder. Skapa en fil env.d.ts i projektroten:

// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    NEXT_PUBLIC_APP_URL: string;
  }
}

Nu är process.env.DATABASE_URL typat som string i hela projektet, utan att du behöver skriva as string på varje ställe.

En viktig begränsning: TypeScript-typer existerar bara vid kompilering. Den här deklarationen talar om för kompilatorn att variabeln finns, men den kontrollerar inte att den faktiskt är satt när appen startar. Kombinera gärna med en Zod-validering som körs vid uppstart, exempelvis i en lib/env.ts-fil som importeras tidigt i applikationen.

Typa API-svar

Använd Zod för att validera och härleda typen i ett steg. Det eliminerar diskrepansen mellan vad TypeScript tror att API:et returnerar och vad det faktiskt returnerar:

import { z } from "zod";

const ProduktSchema = z.object({
  id: z.string(),
  namn: z.string(),
  pris: z.number(),
});

type Produkt = z.infer<typeof ProduktSchema>;

const data = ProduktSchema.parse(await res.json());

parse kastar ett undantag om svaret inte matchar schemat. Det är oftast önskvärt: hellre ett tydligt fel vid API-gränsen än ett tyst typfel djupt inne i renderingen. Om du föredrar att hantera felet utan undantag använder du safeParse istället, som returnerar { success: true, data } eller { success: false, error }.

Delade typer mellan server och klient

Skapa en katalog types/ i projektroten med en index.ts som exporterar de typer som används på flera ställen i applikationen:

// types/index.ts
export type Produkt = {
  id: string;
  namn: string;
  pris: number;
};

export type AnvandarRoll = "admin" | "redaktor" | "lasare";

Importera sedan dessa typer i både Server Components och Client Components:

import type { Produkt } from "@/types";

Filen ska bara innehålla typer och interface, inga runtime-värden. Om du lägger konstanter eller funktioner där riskerar du att klientkomponenter drar in kod som bara bör köras på servern, eller tvärtom. Håll typfilen ren och importera runtime-logik separat.

En liten men viktig detalj: använd import type när du importerar rena typer. Det garanterar att importen försvinner helt vid kompilering och aldrig påverkar bundelstorlek eller exekveringsordning.

Kevin Sommerstein
Kevin SommersteinGrundare, Developly

Medgrundare av Developly Sweden och webbutvecklare med 8 års erfarenhet inom JavaScript, React och Next.js.

LinkedIn →