返回首页

TypeScript 合约 Next.js App Router

本文展示了在 Next.js App Router 中将 TypeScript 用作合约系统。服务器-客户端 payload 类型、params 规范化、domain entities 和 union-states 在应用边界处排除无效状态。

Next.js 中的 TypeScript 作为合约:真实示例
Advertisement 728x90

TypeScript 在 Next.js App Router 中作为契约系统

在 Next.js App Router 中,TypeScript 充当层与层之间的契约系统:服务器-客户端、外部输入-领域、输入-实体、变更-UI。类型不是代码之上的注解,而是在边界处消除无效状态。这无需额外代码即可增强项目的健壮性。

关键契约区域:

  • 从服务器组件向客户端组件传递载荷
  • 在输入时解析参数和搜索参数
  • 规范化领域实体
  • 具有可预测状态的变更结果

通过载荷实现服务器到客户端的契约

从服务器向客户端传递数据是一个关键边界。没有类型化的载荷,客户端依赖对结构的猜测,服务器则发送任意对象。

Google AdInline article slot

定义载荷类型:

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

服务器页面严格按照契约组装数据:

// 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: "工作台笔记",
    renderedAt: new Date().toISOString(),
    mode: "server-to-client",
    initialNotes: getNotes(),
  };

  return <HelloClient payload={payload} />;
}

这种方法保证了可序列化和一致性。App Router 中的服务器/客户端边界定义了渲染、依赖关系和数据流。

Google AdInline article slot

在边界处规范化外部输入

参数和搜索参数以 string | string[] | undefined 的形式到达。将原始输入传递到领域会导致类型断言和整个树中的检查。

正确的方法是在边界处解析:

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

用于搜索参数的实用工具:

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

规范化后,代码处理有效值,TypeScript 不会干扰内部逻辑。

领域实体中不可表达的状态

类型排除了逻辑上不可能的组合。实体始终完整且一致:

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

输入类型允许不完整数据:

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

使用默认值组装:

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

UI 处理可预测的对象,无需检查。

TypeScript 错误处理作为契约信号

类型不可分配 表示契约被破坏,而非局部错误:

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

  return getNoteById(noteId);
}

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

可能未定义 通过守卫解决:

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

  if (!noteId) return null;

  return getNoteById(noteId);
}

提前返回为 TypeScript 缩小类型。

用于操作状态的联合类型

替代 isLoading/hasError 标志,使用可区分联合:

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

自动类型缩小:

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

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

  return state.data.title;
}

逻辑上不可能的状态被排除。

关键要点:

  • TypeScript 在服务器/客户端、参数/领域、输入/实体边界强制执行契约
  • 不可表达的状态消除了 UI 中的运行时错误
  • TypeScript 错误信号表示契约破坏,而非局部问题
  • 联合类型替代标志表示操作状态
  • 在边界处规范化外部输入简化了内部代码

— Editorial Team

Advertisement 728x90

继续阅读