TypeScript en NextJS 16
NextJS tiene soporte nativo de TypeScript. No necesitas configurar nada extra: create-next-app lo incluye por defecto.
Configuracion
El tsconfig.json generado por create-next-app ya tiene todo lo necesario:
json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
El alias @/* te permite importar desde la raiz del proyecto:
tsx
import { db } from "@/lib/database"
import Header from "@/components/Header"
Tipos utiles de Next.js
Metadata
tsx
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Mi Pagina",
description: "Descripcion",
}
Page props con params async
v16Cambio En v16, params y searchParams son Promise:
tsx
// app/productos/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>
searchParams: Promise<{ tab?: string }>
}
export default async function ProductoPage({ params, searchParams }: PageProps) {
const { id } = await params
const { tab } = await searchParams
const producto = await getProducto(id)
return (
<div>
<h1>{producto.nombre}</h1>
{tab === "reviews" && <Reviews productoId={id} />}
</div>
)
}
generateMetadata con params async
tsx
import type { Metadata } from "next"
interface Props {
params: Promise<{ id: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params
const producto = await getProducto(id)
return {
title: producto.nombre,
description: producto.descripcion,
}
}
Layout props
tsx
interface LayoutProps {
children: React.ReactNode
params: Promise<{ locale: string }>
}
export default async function Layout({ children, params }: LayoutProps) {
const { locale } = await params
return <div lang={locale}>{children}</div>
}
Route Handler params
tsx
import { NextResponse } from "next/server"
interface RouteContext {
params: Promise<{ id: string }>
}
export async function GET(request: Request, context: RouteContext) {
const { id } = await context.params
const producto = await db.producto.findUnique({ where: { id } })
if (!producto) {
return NextResponse.json({ error: "No encontrado" }, { status: 404 })
}
return NextResponse.json(producto)
}
Server Actions
tsx
"use server"
import { z } from "zod"
const schema = z.object({
nombre: z.string().min(2),
email: z.string().email(),
})
type ActionState = {
error?: Record<string, string[]>
success?: boolean
} | null
export async function submitForm(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const result = schema.safeParse({
nombre: formData.get("nombre"),
email: formData.get("email"),
})
if (!result.success) {
return { error: result.error.flatten().fieldErrors }
}
await saveToDatabase(result.data)
return { success: true }
}
Tipos para tus modelos
Define tus tipos de datos en archivos separados:
tsx
// types/producto.ts
export interface Producto {
id: string
nombre: string
precio: number
descripcion: string | null
categoriaId: string
createdAt: Date
updatedAt: Date
}
export interface ProductoConCategoria extends Producto {
categoria: {
id: string
nombre: string
}
}
export type CrearProductoInput = Pick<Producto, "nombre" | "precio" | "descripcion" | "categoriaId">
tsx
// Uso
import type { ProductoConCategoria } from "@/types/producto"
async function getProductos(): Promise<ProductoConCategoria[]> {
"use cache"
return await db.producto.findMany({
include: { categoria: true },
})
}
Ejemplo: tipado completo de una pagina dinamica
tsx
// types/post.ts
export interface Post {
slug: string
title: string
content: string
excerpt: string
publishedAt: string
author: {
name: string
avatar: string
}
tags: string[]
}
// lib/posts.ts
import type { Post } from "@/types/post"
export async function getPost(slug: string): Promise<Post | null> {
"use cache"
const res = await fetch(`https://api.example.com/posts/${slug}`)
if (!res.ok) return null
return res.json()
}
export async function getAllSlugs(): Promise<string[]> {
"use cache"
const res = await fetch("https://api.example.com/posts?fields=slug")
const posts: { slug: string }[] = await res.json()
return posts.map((p) => p.slug)
}
// app/blog/[slug]/page.tsx
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import { getPost, getAllSlugs } from "@/lib/posts"
interface PageProps {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
if (!post) return { title: "No encontrado" }
return {
title: post.title,
description: post.excerpt,
}
}
export async function generateStaticParams() {
const slugs = await getAllSlugs()
return slugs.map((slug) => ({ slug }))
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound()
return (
<article>
<header>
<h1 className="text-4xl font-bold">{post.title}</h1>
<div className="flex items-center gap-2 mt-4">
<span className="text-gray-400">{post.author.name}</span>
<span className="text-gray-600">·</span>
<time className="text-gray-400">{post.publishedAt}</time>
</div>
<div className="flex gap-2 mt-2">
{post.tags.map((tag) => (
<span key={tag} className="text-xs bg-gray-800 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
</header>
<div className="mt-8 prose prose-invert">{post.content}</div>
</article>
)
}
Todo tipado de punta a punta: desde el tipo del post, hasta los params de la pagina, pasando por la metadata.