TypeScript als Vertragssystem im Next.js App Router
Im Next.js App Router fungiert TypeScript als Vertragssystem zwischen den Ebenen: Server-Client, externe Eingabe-Domäne, Eingabe-Entität, Mutation-UI. Anstelle von Annotationen über dem Code eliminieren Typen ungültige Zustände an den Grenzen. Dies erhöht die Projektstabilität ohne zusätzlichen Code.
Wichtige Vertragszonen:
- Übergeben von Nutzdaten von Server- zu Client-Komponenten
- Parsen von Parametern und Suchparametern bei der Eingabe
- Normalisierung von Domänenentitäten
- Mutationsergebnisse mit vorhersehbaren Zuständen
Server-zu-Client-Vertrag über Nutzdaten
Das Übergeben von Daten vom Server zum Client ist eine kritische Grenze. Ohne typisierte Nutzdaten verlässt sich der Client auf Vermutungen über die Struktur, und der Server sendet beliebige Objekte.
Definieren Sie einen Nutzdaten-Typ:
// src/demo/contracts.ts
export type HelloPayload = {
appName: string;
renderedAt: string;
mode: "server-to-client";
initialNotes: DemoNote[];
};
Die Server-Seite setzt Daten streng nach Vertrag zusammen:
// src/app/(demo)/demo-contract/page.tsx
import type { HelloPayload } from "@/demo/contracts";
import { getNotes } from "@/demo/notesStore";
import HelloClient from "./HelloClient";
export default function DemoContractPage() {
const payload: HelloPayload = {
appName: "Workbench Notes",
renderedAt: new Date().toISOString(),
mode: "server-to-client",
initialNotes: getNotes(),
};
return <HelloClient payload={payload} />;
}
Dieser Ansatz garantiert Serialisierbarkeit und Konsistenz. Die Server/Client-Grenze im App Router definiert Rendering, Abhängigkeiten und Datenfluss.
Normalisierung externer Eingaben an der Grenze
Parameter und Suchparameter kommen als string | string[] | undefined an. Das Weiterleiten roher Eingaben in die Domäne führt zu Typzusicherungen und Prüfungen im gesamten Baum.
Der korrekte Ansatz ist das Parsen an der Grenze:
// src/demo/noteId.ts
export function parseNoteId(value: unknown): DemoNoteId | null {
if (typeof value !== "string") return null;
const v = value.trim();
if (!/^n\\d+$/.test(v)) return null;
return v;
}
Hilfsfunktion für Suchparameter:
// src/lib/searchParams.ts
export type StringParamValue = string | string[] | undefined | null;
export function getStringParam(value: StringParamValue): string | undefined {
if (typeof value === "string") return value;
if (Array.isArray(value)) return value[0];
return undefined;
}
Nach der Normalisierung arbeitet der Code mit gültigen Werten, und TypeScript stört die interne Logik nicht.
Nicht ausdrückbare Zustände in Domänenentitäten
Typen schließen logisch unmögliche Kombinationen aus. Entitäten sind immer vollständig und konsistent:
type DemoNoteStatus = "draft" | "published" | "archived";
type DemoNotePriority = 1 | 2 | 3;
type DemoNote = {
id: DemoNoteId;
title: string;
status: DemoNoteStatus;
priority: DemoNotePriority;
tags: string[];
description: string;
createdAt: IsoDateString;
updatedAt: IsoDateString;
};
Eingabetyp erlaubt unvollständige Daten:
type DemoNoteCreateInput = {
title: string;
status?: DemoNoteStatus;
priority?: DemoNotePriority;
tags?: string[];
description?: string;
};
Zusammenbau mit Standardwerten:
// src/demo/notesStore.ts
export function createNote(input: DemoNoteCreateInput): DemoNote {
const id: DemoNoteId = `n${Date.now()}`;
const t = new Date().toISOString();
const note: DemoNote = {
id,
title: input.title.trim(),
status: input.status ?? "draft",
priority: input.priority ?? 2,
tags: input.tags ?? ["new"],
description: input.description ?? "",
createdAt: t,
updatedAt: t,
};
notes = [note, ...notes];
return note;
}
Die UI arbeitet mit vorhersehbaren Objekten ohne Prüfungen.
TypeScript-Fehlerbehandlung als Vertragssignale
Typ ist nicht zuweisbar signalisiert einen gebrochenen Vertrag, keinen lokalen Fehler:
// Korrekt
export function demo_id_barrier(raw: string) {
const noteId = parseNoteId(raw);
if (!noteId) return null;
return getNoteById(noteId);
}
// Falsch
export function demo_bad_fix(raw: string) {
const unsafeId = raw as any;
return getNoteById(unsafeId);
}
Möglicherweise undefined wird mit Guards aufgelöst:
export function demo_parseNoteId(raw: string) {
const noteId = parseNoteId(raw);
if (!noteId) return null;
return getNoteById(noteId);
}
Frühe Rückgabe verengt den Typ für TypeScript.
Union-Typen für Operationszustände
Anstelle von isLoading/hasError-Flags verwenden Sie diskriminierte Unions:
export type ActionResult<T> =
| { ok: true; data: T }
| { ok: false; error: string };
export function ok<T>(data: T): ActionResult<T> {
return { ok: true, data };
}
export function fail(error: string): ActionResult<never> {
return { ok: false, error };
}
Automatische Typverengung:
export function demo_narrowing() {
const state = createResult(Math.random() > 0.5);
if (!state.ok) {
return state.error;
}
return state.data.title;
}
Unlogische Zustände sind unmöglich.
Wichtige Erkenntnisse:
- TypeScript erzwingt Verträge an Server/Client-, Parameter/Domänen-, Eingabe/Entitäts-Grenzen
- Nicht ausdrückbare Zustände eliminieren Laufzeitfehler in der UI
- TypeScript-Fehler signalisieren gebrochene Verträge, keine lokalen Probleme
- Union-Typen ersetzen Flags durch Operationszustände
- Normalisierung externer Eingaben an der Grenze vereinfacht internen Code
— Editorial Team
Noch keine Kommentare.