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.