Retour à l'accueil

Contrats TypeScript Next.js App Router

L'article montre l'application de TypeScript comme système de contrats dans Next.js App Router. Types payload server-client, normalisation des params, entités de domaine et union-états excluent les états invalides aux frontières de l'application.

TypeScript comme Contrats dans Next.js : exemples réels
Advertisement 728x90

TypeScript comme système de contrat dans le routeur d'application Next.js

Dans le routeur d'application Next.js, TypeScript agit comme un système de contrat entre les couches : serveur-client, entrée externe-domaine, entité-entrée, mutation-interface utilisateur. Plutôt que des annotations superposées au code, les types éliminent les états invalides aux frontières. Cela renforce la résilience du projet sans code supplémentaire.

Zones contractuelles clés :

  • Transmission de la charge utile du composant serveur au composant client
  • Analyse des paramètres et searchParams en entrée
  • Normalisation des entités du domaine
  • Résultats de mutation avec des états prévisibles

Contrat serveur-client via la charge utile

La transmission des données du serveur au client est une frontière critique. Sans charge utile typée, le client s'appuie sur des suppositions concernant la structure, et le serveur envoie des objets arbitraires.

Google AdInline article slot

Définissez un type de charge utile :

// src/demo/contracts.ts
export type HelloPayload = {
  appName: string;
  renderedAt: string;
  mode: "server-to-client";
  initialNotes: DemoNote[];
};

La page serveur assemble les données strictement selon le contrat :

// 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} />;
}

Cette approche garantit la sérialisabilité et la cohérence. La frontière serveur/client dans le routeur d'application définit le rendu, les dépendances et le flux de données.

Google AdInline article slot

Normalisation des entrées externes à la frontière

Les paramètres et searchParams arrivent sous forme de string | string[] | undefined. Passer l'entrée brute dans le domaine conduit à des assertions de type et des vérifications tout au long de l'arborescence.

L'approche correcte est l'analyse à la frontière :

// 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;
}

Utilitaire pour searchParams :

Google AdInline article slot
// 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;
}

Après normalisation, le code fonctionne avec des valeurs valides, et TypeScript n'interfère pas avec la logique interne.

États inexprimables dans les entités du domaine

Les types excluent les combinaisons logiquement impossibles. Les entités sont toujours complètes et cohérentes :

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

Le type d'entrée autorise des données incomplètes :

type DemoNoteCreateInput = {
  title: string;
  status?: DemoNoteStatus;
  priority?: DemoNotePriority;
  tags?: string[];
  description?: string;
};

Assemblage avec valeurs par défaut :

// 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;
}

L'interface utilisateur fonctionne avec des objets prévisibles sans vérifications.

Gestion des erreurs TypeScript comme signaux de contrat

Le type n'est pas assignable signale un contrat rompu, pas une erreur locale :

// Correct
export function demo_id_barrier(raw: string) {
  const noteId = parseNoteId(raw);
  if (!noteId) return null;

  return getNoteById(noteId);
}

// Incorrect
export function demo_bad_fix(raw: string) {
  const unsafeId = raw as any;
  return getNoteById(unsafeId);
}

Potentiellement indéfini est résolu avec des gardes :

export function demo_parseNoteId(raw: string) {
  const noteId = parseNoteId(raw);

  if (!noteId) return null;

  return getNoteById(noteId);
}

Le retour anticipé affine le type pour TypeScript.

Types union pour les états d'opération

Au lieu des drapeaux isLoading/hasError, utilisez une union discriminée :

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

Affinage automatique du type :

export function demo_narrowing() {
  const state = createResult(Math.random() > 0.5);

  if (!state.ok) {
    return state.error;
  }

  return state.data.title;
}

Les états illogiques sont impossibles.

Points clés à retenir :

  • TypeScript applique des contrats aux frontières serveur/client, paramètres/domaine, entrée/entité
  • Les états inexprimables éliminent les erreurs d'exécution dans l'interface utilisateur
  • Les erreurs TypeScript signalent des contrats rompus, pas des problèmes locaux
  • Les types union remplacent les drapeaux par des états d'opération
  • La normalisation des entrées externes à la frontière simplifie le code interne

— Editorial Team

Advertisement 728x90

Lire ensuite