Next.js
TypeScript en Next.js
Explica cómo tipar rutas, DTOs, Actions, configuración y estados en Next.js, diferenciando seguridad compile-time de validación runtime.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo tipar rutas, DTOs, Actions, configuración y estados en Next.js, diferenciando seguridad compile-time de validación runtime.
TypeScript ayuda a describir rutas, componentes, DTOs, Actions y configuración, pero solo existe durante desarrollo/build. Todo dato que llega desde URL, FormData, cookie, request, base de datos no controlada o API externa sigue siendo unknown en runtime hasta validarse.
compile-time type
→ prevents invalid code paths before deploy
runtime schema
→ proves external value has expected shape
DTO/domain type
→ carries validated meaning through the appNo confundas as Input con validación.
Next.js genera y mantiene next-env.d.ts; no lo edites. Mantén strict: true y alias claros:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"paths": { "@/*": ["./src/*"] }
}
}Activa opciones según capacidad del equipo; no silencies miles de errores sin una migración gradual.
Los helpers generados permiten expresar rutas:
export default async function Page({
params,
searchParams,
}: PageProps<"/products/[slug]">) {
const { slug } = await params;
const query = await searchParams;
}En el modelo actual, params/search params pueden ser Promises. Verifica tipos generados por tu versión y ejecuta next typegen o build cuando corresponda.
Con configuración compatible, Link y router pueden validar rutas literales:
<Link href="/portfolio">Portfolio</Link>Para valores dinámicos, conserva tipos mediante helpers y evita cast global a Route solo para silenciar errores. Una URL válida sintácticamente todavía puede apuntar a un recurso inexistente.
Una Client Component recibe datos serializables:
type ProjectCardProps = {
project: {
id: string;
title: string;
publishedAt: string;
};
};No pases clientes DB, funciones normales, clases o entidades con campos privados. Convierte Dates a un contrato explícito cuando cruza la boundary.
type UserRecord = {
passwordHash: string;
recoverySecret: string;
// ...
};
type UserNavView = {
id: string;
displayName: string;
avatarUrl: string | null;
};No reutilices el tipo ORM como prop. El DTO minimiza datos y desacopla UI de persistencia.
const inputSchema = z.object({
quantity: z.coerce.number().int().min(1).max(100),
productId: z.string().uuid(),
});
const result = inputSchema.safeParse({
quantity: formData.get("quantity"),
productId: formData.get("productId"),
});Después del parse, TypeScript conoce el tipo y runtime comprobó el valor.
Valida también respuestas externas: el SDK puede estar desactualizado o el proveedor romper contrato.
type MutationState =
| { status: "idle" }
| { status: "invalid"; fieldErrors: Record<string, string[]> }
| { status: "conflict"; currentVersion: number }
| { status: "success"; id: string };Evita combinaciones imposibles como isLoading=true, data presente y error presente simultáneamente.
Haz switches exhaustivos:
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}"use server";
export async function createOrder(
_previous: CreateOrderState,
formData: FormData,
): Promise<CreateOrderState> {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { status: "invalid", fieldErrors: parsed.error.flatten().fieldErrors };
return orderService.create(parsed.data);
}El return type obliga a mantener el contrato. No exportes funciones auxiliares desde un módulo use server si no son Server Functions compatibles.
const body: unknown = await request.json();
const input = schema.parse(body);Incorrecto:
const input = (await request.json()) as CreateOrderInput;El cast no inspecciona nada.
const envSchema = z.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
});
export const env = envSchema.parse(process.env);Separa server/public env para impedir imports accidentales al cliente.
const metadata = {
title: "Portfolio",
description: "Proyectos de desarrollo web",
} satisfies Metadata;Valida forma sin ampliar innecesariamente literales. También es útil en config y mapas de variantes.
unknown obliga a comprobar. any desactiva el sistema y se propaga. Usa any solo en una frontera muy controlada y documentada cuando una librería carece de tipos.
No conviertas cada componente en una API genérica compleja. Úsalos cuando preservan relación entre inputs/outputs:
async function parseJson<T>(response: Response, schema: Schema<T>): Promise<T> {
return schema.parse(await response.json());
}El schema, no el generic, valida runtime.
Un error al pasar función servidor a cliente puede indicar boundary incorrecta. Un tipo enorme lleno de opcionales puede revelar un DTO universal mal diseñado. No resuelvas todos con casts; revisa ownership y flujo.
ORM, OpenAPI y GraphQL pueden generar tipos. Fija versiones y genera en CI. El código generado representa schema, no autorización ni reglas de negocio.
tsc --noEmit.Un test TypeScript no prueba que un proveedor entregue el payload correcto.
Falsa confianza.
Fuga/acoplamiento.
Estados imposibles.
Contamina toda la app.
Oculta rutas inválidas.
No valida requests.
Divergen del contrato real.
satisfies valida sin perder inferencia.request.json() as Input?ESLint, formatting y calidad automatiza invariantes que los tipos no cubren.