TypeScript + React: Lo Básico
14 ejemplos para empezar con TypeScript + React -- 9 básicos y 5 intermedios.
Busca en todas las páginas de la documentación
14 ejemplos para empezar con TypeScript + React -- 9 básicos y 5 intermedios.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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:
.tsx. Plain .ts es solo para módulos sin JSX.strict: true en tsconfig.json -- que es el valor predeterminado en nuevos scaffolds de Next.js y Vite.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.
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>
);
}<NombreComponente>Props por convención -- fácil de encontrar, fácil de extender.({ name, age }: GreetingProps).interface para props de componentes -- la fusión de declaraciones hace que sea fácil de extender; los alias type también funcionan bien.React.FC -- agrega un children implí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
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 agrega undefined a su tipo.variant = "primary") rellenan cuando el llamador omite la prop."primary" | "secondary") restringe a los llamadores a opciones válidas -- el autocompletado funciona de inmediato.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
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>
);
}ReactNode es el tipo "renderizable" más amplio -- cadenas, números, elementos, arrays, fragmentos y null.ReactElement cuando específicamente necesitas un solo elemento JSX (p. ej., para clonar).type (import type { ReactNode }) -- las importaciones de solo tipo se borran en tiempo de ejecución, manteniendo el paquete limpio.children como 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
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>
);
}useState(inicial) tiene suficiente información -- useState(0) ya es number.useState<T>(null) cuando el valor podría ser algo más rico más adelante -- de lo contrario TS lo fija a null.User | null hacen que el estado "no cargado aún" sea explícito -- los consumidores deben estrechar antes de usar campos.Relacionado: Tipificación de State -- formas de state complejas, state discriminada | useState -- el hook subyacente
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>
</>
);
}ChangeEvent<HTMLInputElement>, MouseEvent<HTMLButtonElement>.KeyboardEvent<HTMLInputElement> y lee e.key.FormEvent<HTMLFormElement> y recuerda e.preventDefault().onChange.Relacionado: Tipificación de Eventos -- cada tipo de evento, patrones de formularios | Eventos de Ratón / Eventos de Formulario -- las APIs subyacentes
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 } -- el null coincide con el tipo propio de React.inputRef.current?.focus()) -- la ref es null antes de que el elemento se monte.useRef<T | null>(null) y asigna cuando sea necesario.HTMLInputElement, no HTMLElement) para mantener .value, .checked, etc. tipificados.Relacionado: Tipificación de Refs -- refs del DOM, refs mutables,
forwardRef| useRef -- el hook
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}`;
}kind) permite a TypeScript estrechar la unión con una sola verificación.typeof value === "string" estrecha primitivos; "field" in obj estrecha formas de objeto sin un discriminador.Array.isArray(x) e instanceof funcionan también -- TS sabe que cada uno de estos estrecha el tipo.Relacionado: Estrechamiento de Tipos -- in, typeof, instanceof, funciones de aserción | Uniones Discriminadas -- el patrón que hace que el estrechamiento sea limpio
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 erroras es el discriminador -- cada valor lleva props requeridas/prohibidas diferentes.never hace que sea un error de compilación mezclar modos.as.Relacionado: Uniones Discriminadas -- patrones de diseño, estrechamiento de tipos | Tipificación de Props -- formas de prop avanzadas
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> y Record<K, V> completan las utilidades más utilizadas.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
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>}
/>
);
}function List<T>(...)) y reutiliza T en todo el props.T del primer argumento que lo lleva -- los llamadores raramente escriben el genérico explícitamente.extends para restringir el genérico: <T extends { id: string | number }> te permite usar id sin necesidad de keyOf.Relacionado: Genéricos en React -- hooks genéricos, restricciones, forwardRef | Tipos Utilitarios -- composición de utilidades con genéricos
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;
}undefined y envuelve useContext en un hook personalizado que lanza -- los consumidores obtienen un valor no nulo.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.Relacionado: Tipificación de Context -- proveedores, contextos divididos, selectores | useContext -- el hook
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
}Promise<User> es una mentira si nunca validas -- la API podría devolver cualquier cosa.unknown hasta que pase el esquema -- sin any accidental filtrándose.queryFn de 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
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 });
}params y searchParams son Promises -- siempre await antes de leer claves.PageProps / Ctx por ruta; no alcances un AppPageProps genérico.state y FormData de la acción para evitar any.Request (Web-estándar); devuelve NextResponse.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
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.npm install --save-dev @types/<paquete>) antes de escribir la tuya propia..d.ts ambientes pequeños y bien nombrados; una declaración hecha a mano que se desincroniza es peor que any.declare global para globales en tiempo de ejecución (p. ej., aumentar window); 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
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥