Volver al inicio

TypeScript Contratos Next.js App Router

El artículo muestra la aplicación de TypeScript como sistema de contratos en Next.js App Router. Tipos de payload server-client, normalización de params, entidades de dominio y estados-unión excluyen estados inválidos en las fronteras de la aplicación.

TypeScript como Contratos en Next.js: ejemplos reales
Advertisement 728x90

TypeScript como sistema de contratos en el App Router de Next.js

En el App Router de Next.js, TypeScript actúa como un sistema de contratos entre capas: servidor-cliente, entrada externa-dominio, entrada-entidad, mutación-interfaz. En lugar de anotaciones sobre el código, los tipos eliminan estados inválidos en los límites. Esto mejora la resiliencia del proyecto sin código adicional.

Zonas clave de contrato:

  • Pasar datos del componente Servidor al componente Cliente
  • Analizar parámetros y searchParams en la entrada
  • Normalizar entidades del dominio
  • Resultados de mutación con estados predecibles

Contrato servidor-cliente mediante datos

Pasar datos del servidor al cliente es un límite crítico. Sin un tipo definido para los datos, el cliente depende de conjeturas sobre la estructura, y el servidor envía objetos arbitrarios.

Google AdInline article slot

Define un tipo para los datos:

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

La página del servidor ensambla los datos estrictamente por contrato:

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

Este enfoque garantiza serialización y consistencia. El límite servidor/cliente en App Router define el renderizado, las dependencias y el flujo de datos.

Google AdInline article slot

Normalizar entrada externa en el límite

Los parámetros y searchParams llegan como string | string[] | undefined. Pasar entrada sin procesar al dominio conduce a aserciones de tipo y verificaciones en todo el árbol.

El enfoque correcto es analizar en el límite:

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

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

Después de la normalización, el código trabaja con valores válidos, y TypeScript no interfiere con la lógica interna.

Estados inexpresables en entidades del dominio

Los tipos excluyen combinaciones lógicamente imposibles. Las entidades siempre están completas y son consistentes:

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

El tipo de entrada permite datos incompletos:

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

Ensamblaje con valores por defecto:

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

La interfaz trabaja con objetos predecibles sin verificaciones.

Manejo de errores de TypeScript como señales de contrato

El tipo no es asignable señala un contrato roto, no un error local:

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

  return getNoteById(noteId);
}

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

Posiblemente undefined se resuelve con guardias:

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

  if (!noteId) return null;

  return getNoteById(noteId);
}

El retorno temprano estrecha el tipo para TypeScript.

Tipos unión para estados de operación

En lugar de banderas isLoading/hasError, usa unión discriminada:

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

Estrechamiento automático de tipo:

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

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

  return state.data.title;
}

Los estados ilógicos son imposibles.

Conclusiones clave:

  • TypeScript hace cumplir contratos en los límites servidor/cliente, parámetros/dominio, entrada/entidad
  • Los estados inexpresables eliminan errores en tiempo de ejecución en la interfaz
  • Los errores de TypeScript señalan contratos rotos, no problemas locales
  • Los tipos unión reemplazan banderas con estados de operación
  • Normalizar la entrada externa en el límite simplifica el código interno

— Editorial Team

Advertisement 728x90

Leer después