TypeScript + React: Lo Básico
14 ejemplos para empezar con TypeScript + React -- 9 básicos y 5 intermedios.
Requisitos previos
Un proyecto estándar de React con TypeScript es suficiente. Para Next.js:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-appConvenciones utilizadas en todo el documento:
- Cada archivo de React es
.tsx. Plain.tses solo para módulos sin JSX. strict: trueentsconfig.json-- que es el valor predeterminado en nuevos scaffolds de Next.js y Vite.- Importa tipos compartidos de
react(ReactNode,ComponentType,FormEvent, etc.) en lugar de redeclarar los.
¿Nuevo en TypeScript con React? Consulta Fundamentos de TypeScript para React para la introducción conceptual a tipos, interfaces y tsconfig.
Ejemplos Básicos
1. Tipificación de Props de Componentes
Define la forma de las props de un componente como una interfaz de TypeScript.
interface GreetingProps {
name: string;
age: number;
}
function Greeting({ name, age }: GreetingProps) {
return (
<p>
{name} is {age}
</p>
);
}- Nombra la interfaz
<NombreComponente>Propspor convención -- fácil de encontrar, fácil de extender. - Desestructura en la lista de parámetros para acceso limpio:
({ name, age }: GreetingProps). - Prefiere
interfacepara props de componentes -- la fusión de declaraciones hace que sea fácil de extender; los aliastypetambién funcionan bien. - Nunca uses
React.FC-- agrega unchildrenimplícito que a menudo no deseas y está cayendo en desuso en 2026.
Relacionado: Tipificación de Props -- props opcionales, children, uniones discriminadas | Componentes (Fundamentos) -- props en profundidad
2. Props Opcionales y Valores Predeterminados
Marca props como opcionales con ?, luego suministra un valor predeterminado en la desestructuración.
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
}
function Button({ label, variant = "primary", disabled = false }: ButtonProps) {
return (
<button className={variant} disabled={disabled}>
{label}
</button>
);
}variant?: ...hace que la prop sea opcional; TypeScript agregaundefineda su tipo.- Los valores predeterminados en la desestructuración (
variant = "primary") rellenan cuando el llamador omite la prop. - Un tipo de unión literal (
"primary" | "secondary") restringe a los llamadores a opciones válidas -- el autocompletado funciona de inmediato. - No uses
defaultProps-- está deprecado para componentes de función en React 19.
Relacionado: Tipificación de Props -- uniones, encadenamiento opcional, props de variante | Uniones Discriminadas -- cuando las variantes llevan diferentes campos
3. La Prop children
Acepta cualquier contenido renderizable con React.ReactNode.
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}ReactNodees el tipo "renderizable" más amplio -- cadenas, números, elementos, arrays, fragmentos ynull.- Usa
ReactElementcuando específicamente necesitas un solo elemento JSX (p. ej., para clonar). - Importa como
type(import type { ReactNode }) -- las importaciones de solo tipo se borran en tiempo de ejecución, manteniendo el paquete limpio. - Para APIs de render prop, tipifica
childrencomo una función:children: (state: T) => ReactNode.
Relacionado: Tipificación de Props -- variantes de children (elemento, función, array) | Composición -- children como primitiva de composición
4. Tipificación de useState
Deja que la inferencia haga el trabajo, y proporciona un tipo explícito cuando el valor inicial no es suficiente.
import { useState } from "react";
interface User {
id: string;
name: string;
}
function UserPanel() {
// Inferido como number del valor inicial
const [count, setCount] = useState(0);
// Genérico explícito necesario cuando el valor inicial no lleva el tipo completo
const [user, setUser] = useState<User | null>(null);
return (
<p>
{count} — {user?.name ?? "no user"}
</p>
);
}- Omite el genérico cuando
useState(inicial)tiene suficiente información --useState(0)ya esnumber. - Suministra
useState<T>(null)cuando el valor podría ser algo más rico más adelante -- de lo contrario TS lo fija anull. - Las uniones como
User | nullhacen que el estado "no cargado aún" sea explícito -- los consumidores deben estrechar antes de usar campos. - No almacenes valores derivados en state. Calcula durante el renderizado desde state más simple.
Relacionado: Tipificación de State -- formas de state complejas, state discriminada | useState -- el hook subyacente
5. Tipificación de Manejadores de Eventos
Usa los tipos de evento genéricos que React exporta para que e.target, e.key y amigos permanezcan tipificados.
import type { ChangeEvent, MouseEvent } from "react";
import { useState } from "react";
export default function SearchBar() {
const [q, setQ] = useState("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setQ(e.target.value);
};
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log("pressed at", e.clientX, e.clientY);
};
return (
<>
<input value={q} onChange={handleChange} />
<button onClick={handleClick}>Search</button>
</>
);
}- Los tipos de evento son genéricos en el elemento objetivo:
ChangeEvent<HTMLInputElement>,MouseEvent<HTMLButtonElement>. - Para interacciones de teclado usa
KeyboardEvent<HTMLInputElement>y leee.key. - Para formularios usa
FormEvent<HTMLFormElement>y recuerdae.preventDefault(). - Si mantienes el manejador en línea, puedes omitir la anotación -- TypeScript lo infiere de la propia firma de
onChange.
Relacionado: Tipificación de Eventos -- cada tipo de evento, patrones de formularios | Eventos de Ratón / Eventos de Formulario -- las APIs subyacentes
6. useRef para un Nodo del DOM
Tipifica useRef con el elemento que referenciará, inicializado a null.
import { useEffect, useRef } from "react";
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}useRef<HTMLInputElement>(null)da{ current: HTMLInputElement | null }-- elnullcoincide con el tipo propio de React.- Siempre acceso opcional (
inputRef.current?.focus()) -- la ref esnullantes de que el elemento se monte. - Para valores mutables (temporizadores, cálculos en caché), usa
useRef<T | null>(null)y asigna cuando sea necesario. - Elige el tipo de elemento preciso (
HTMLInputElement, noHTMLElement) para mantener.value,.checked, etc. tipificados.
Relacionado: Tipificación de Refs -- refs del DOM, refs mutables,
forwardRef| useRef -- el hook
7. Estrechamiento de Tipos con in, typeof y Discriminadores
Convierte una unión en una rama específica para que los campos sean seguros de acceder.
type Response =
| { kind: "ok"; data: string }
| { kind: "error"; message: string };
function render(res: Response): string {
if (res.kind === "ok") {
// res ahora es { kind: "ok"; data: string }
return `Got: ${res.data}`;
}
return `Failed: ${res.message}`;
}- Una propiedad discriminador (
kind) permite a TypeScript estrechar la unión con una sola verificación. typeof value === "string"estrecha primitivos;"field" in objestrecha formas de objeto sin un discriminador.Array.isArray(x)einstanceoffuncionan también -- TS sabe que cada uno de estos estrecha el tipo.- Exhaustividad: maneja cada rama para que agregar una nueva fuerce un error de compilación en cada sitio de llamada.
Relacionado: Estrechamiento de Tipos -- in, typeof, instanceof, funciones de aserción | Uniones Discriminadas -- el patrón que hace que el estrechamiento sea limpio
8. Props de Unión Discriminada
Codifica "este modo u otro modo" para que combinaciones de props inválidas fallen en tiempo de compilación.
type LinkProps =
| { as: "button"; onClick: () => void; href?: never }
| { as: "anchor"; href: string; onClick?: never };
function ActionLink(props: LinkProps) {
if (props.as === "button") {
return <button onClick={props.onClick}>Click</button>;
}
return <a href={props.href}>Click</a>;
}
// <ActionLink as="button" onClick={...} /> ✓
// <ActionLink as="anchor" href="..." /> ✓
// <ActionLink as="anchor" onClick={...} /> ✗ type error- El campo
ases el discriminador -- cada valor lleva props requeridas/prohibidas diferentes. - Marcar el campo opuesto como
neverhace que sea un error de compilación mezclar modos. - Prefiere una unión discriminada sobre "props opcionales + verificación en tiempo de ejecución" -- las llamadas inválidas fallan antes de que se envíen.
- El patrón se escala: botones, entradas y tarjetas se benefician todas de uniones
as.
Relacionado: Uniones Discriminadas -- patrones de diseño, estrechamiento de tipos | Tipificación de Props -- formas de prop avanzadas
9. Tipos Utilitarios (Pick, Omit, Partial)
Deriva nuevos tipos de existentes en lugar de escribir dos interfaces que deriven.
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
type UserSummary = Pick<User, "id" | "name">;
type NewUser = Omit<User, "id" | "createdAt">;
type UserUpdate = Partial<Omit<User, "id">>;Pick<T, K>mantiene solo las claves listadas;Omit<T, K>las elimina -- compón para expresar formas de API.Partial<T>hace cada propiedad opcional; práctico para puntos finales PATCH y state de formularios.Required<T>,Readonly<T>yRecord<K, V>completan las utilidades más utilizadas.- Cambiar la interfaz de origen fluye automáticamente -- sin duplicados sincronizados manualmente.
Relacionado: Tipos Utilitarios para React -- cada utilidad con ejemplos de React | Tipificación de Respuestas de API -- uso de utilidades en límites de API
Ejemplos Intermedios
10. Componentes Genéricos
Deja que el llamador especifique el tipo de elemento para que el componente permanezca tipificado para cualquier lista.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyOf: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyOf(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Uso — T se infiere de `items`
function UsersPage({ users }: { users: { id: string; name: string }[] }) {
return (
<List
items={users}
keyOf={(u) => u.id}
renderItem={(u) => <span>{u.name}</span>}
/>
);
}- Declara el genérico en el componente (
function List<T>(...)) y reutilizaTen todo el props. - TypeScript infiere
Tdel primer argumento que lo lleva -- los llamadores raramente escriben el genérico explícitamente. - Usa
extendspara restringir el genérico:<T extends { id: string | number }>te permite usaridsin necesidad dekeyOf. - Funciona para tablas, controles de selección, campos de autocompletado -- cualquier componente "lista de X".
Relacionado: Genéricos en React -- hooks genéricos, restricciones, forwardRef | Tipos Utilitarios -- composición de utilidades con genéricos
11. Context Tipificado
Comparte datos a través de contexto sin una aserción de no nulidad en cada consumidor.
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
interface AuthValue {
user: { id: string; name: string } | null;
login: (name: string) => void;
}
const AuthCtx = createContext<AuthValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthValue["user"]>(null);
const login = (name: string) => setUser({ id: crypto.randomUUID(), name });
return <AuthCtx.Provider value={{ user, login }}>{children}</AuthCtx.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthCtx);
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
return ctx;
}- Inicializa el contexto con
undefinedy envuelveuseContexten un hook personalizado que lanza -- los consumidores obtienen un valor no nulo. - Exporta solo el hook (
useAuth), no el contexto crudo -- los consumidores no pueden eludir la verificación. AuthValue["user"]se indexa en la interfaz para que el setter permanezca en sincronía con la forma.- Para el desempeño, divide los contextos de lectura/escritura (consulta la página de lo básico de React Patterns).
Relacionado: Tipificación de Context -- proveedores, contextos divididos, selectores | useContext -- el hook
12. Fetch Tipificado con Zod
Garantiza en tiempo de ejecución que las respuestas de API coincidan con el tipo de TypeScript que envías.
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: unknown = await res.json();
return UserSchema.parse(json); // valida + estrecha a User
}- Tipificar una respuesta como
Promise<User>es una mentira si nunca validas -- la API podría devolver cualquier cosa. - Un esquema Zod valida en tiempo de ejecución e infiere el tipo de TypeScript, por lo que solo hay una cosa que mantener.
- Trata la respuesta cruda como
unknownhasta que pase el esquema -- sinanyaccidental filtrándose. - Para aplicaciones con muchas consultas, combina con
queryFnde TanStack Query + esquema para que cada consulta en la aplicación esté tipificada de extremo a extremo.
Relacionado: Tipificación de Respuestas de API -- patrones, formas de error, composición de esquema | Fundamentos de Zod -- el primer paso de Zod
13. Tipificación de Server Components y Route Handlers
Next.js 15 hace que params y searchParams sean asincrónicas -- tipifícalas como Promise<T>.
// app/posts/[id]/page.tsx
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ preview?: string }>;
}
export default async function PostPage({ params, searchParams }: PageProps) {
const { id } = await params;
const { preview } = await searchParams;
return <p>Post {id} {preview ? "(preview)" : ""}</p>;
}// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";
interface Ctx {
params: Promise<{ id: string }>;
}
export async function GET(_req: Request, ctx: Ctx) {
const { id } = await ctx.params;
return NextResponse.json({ id });
}- En Next.js 15,
paramsysearchParamsson Promises -- siempreawaitantes de leer claves. - Define una interfaz local
PageProps/Ctxpor ruta; no alcances unAppPagePropsgenérico. - Para el lado del cliente de un formulario, anota explícitamente el
stateyFormDatade la acción para evitarany. - Los route handlers reciben
Request(Web-estándar); devuelveNextResponse.json(body)para respuestas JSON tipificadas.
Relacionado: Tipificación de Server Components -- props, componentes asincrónicas, límites | Tipificación de Route Handlers -- firmas GET/POST/PUT, middleware
14. Archivo de Declaración para un Módulo Sin Tipos
Escribe un .d.ts para agregar tipos a un paquete JavaScript simple que no envía tipos propios.
// types/colorful-logger.d.ts
declare module "colorful-logger" {
export interface LogOptions {
color?: "red" | "green" | "blue";
bold?: boolean;
}
export function log(message: string, options?: LogOptions): void;
export function clear(): void;
}Luego en tsconfig.json:
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}declare module "<nombre>"le dice a TypeScript la superficie pública de un módulo que no tiene tipos incorporados.- Verifica DefinitelyTyped primero (
npm install --save-dev @types/<paquete>) antes de escribir la tuya propia. - Mantén los archivos
.d.tsambientes pequeños y bien nombrados; una declaración hecha a mano que se desincroniza es peor queany. - Usa
declare globalpara globales en tiempo de ejecución (p. ej., aumentarwindow); usa declaraciones de módulo para importaciones.
Relacionado: Archivos de Declaración -- módulo, global y declaraciones ambientes | Patrones de Modo Estricto -- mantente honesto cuando integres deps sin tipos