TypeScript con SWR
Receta
Tipa tus llamadas a useSWR con genéricos para obtener seguridad de tipos completa en data, error, claves y funciones fetcher. El soporte de TypeScript de SWR permite inferir tipos de retorno a partir de las firmas del fetcher.
"use client";
import useSWR from "swr";
interface User {
id: string;
name: string;
email: string;
}
const fetcher = (url: string): Promise<User> =>
fetch(url).then((r) => r.json());
function Profile() {
const { data, error } = useSWR<User, Error>("/api/me", fetcher);
// data: User | undefined
// error: Error | undefined
if (data) {
return <div>{data.name}</div>; // aquí data es User
}
}Ejemplo funcional
"use client";
import useSWR, { Fetcher, SWRConfiguration, Key } from "swr";
import useSWRMutation, { SWRMutationConfiguration } from "swr/mutation";
// ---- Factory de fetcher con tipos seguros ----
function createFetcher<T>(): Fetcher<T, string> {
return async (url: string) => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
};
}
// ---- Tipos de dominio ----
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface CreateProductInput {
name: string;
price: number;
category: string;
}
// ---- Hooks con tipos seguros ----
function useProduct(id: string) {
return useSWR<Product, Error>(
`/api/products/${id}`,
createFetcher<Product>()
);
}
function useProducts(category?: string) {
const key = category ? `/api/products?category=${category}` : "/api/products";
return useSWR<Product[], Error>(key, createFetcher<Product[]>());
}
// ---- Mutación con tipos seguros ----
async function createProduct(
url: string,
{ arg }: { arg: CreateProductInput }
): Promise<Product> {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(arg),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
function useCreateProduct() {
return useSWRMutation<Product, Error, string, CreateProductInput>(
"/api/products",
createProduct
);
}
// ---- Uso ----
export default function ProductPage({ id }: { id: string }) {
const { data: product, error, isLoading } = useProduct(id);
const { trigger, isMutating } = useCreateProduct();
if (isLoading) return <div>Cargando...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{product?.name}</h1>
<p>${product?.price}</p>
<button
onClick={() =>
trigger({ name: "New Item", price: 29.99, category: "widgets" })
}
disabled={isMutating}
>
Añadir producto
</button>
</div>
);
}Profundización
Cómo funciona
useSWR<Data, Error, Key>acepta hasta tres parámetros genéricos: el tipo de datos, el tipo de error y el tipo de clave.- Cuando proporcionas un fetcher tipado, SWR puede inferir el genérico
Dataa partir del tipo de retorno del fetcher. - El tipo
Fetcher<Data, Key>de SWR exige que el fetcher acepte el tipo de clave y devuelvaPromise<Data>. useSWRMutation<Data, Error, Key, Arg>añade un cuarto genérico para el tipo del argumento de mutación.- SWR exporta tipos de utilidad como
SWRResponse,SWRConfiguration,KeyyFetcherpara tipado avanzado.
Variaciones
Tipos inferidos desde el fetcher:
// SWR infiere Data del tipo de retorno del fetcher
const fetchUser = (url: string): Promise<User> => fetch(url).then((r) => r.json());
const { data } = useSWR("/api/me", fetchUser);
// data: User | undefined — inferido desde fetchUserTipado de claves en array:
type FetcherArgs = [url: string, token: string];
const fetcher: Fetcher<User, FetcherArgs> = ([url, token]) =>
fetch(url, { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.json());
const { data } = useSWR<User, Error, FetcherArgs>(
["/api/me", authToken],
fetcher
);Middleware tipado:
import { Middleware, SWRHook } from "swr";
const loggerMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
const result = useSWRNext(key, fetcher, config);
console.log(`SWR [${key}]:`, result.data);
return result;
};
};Configuración de SWR tipada:
import type { SWRConfiguration } from "swr";
const defaultConfig: SWRConfiguration<User, Error> = {
revalidateOnFocus: false,
errorRetryCount: 3,
onSuccess: (data) => {
// data está tipado como User
console.log(data.name);
},
};Notas de TypeScript
- Prefiere siempre genéricos explícitos frente a casts con
aspara seguridad de tipos.useSWR<User>es mejor quedata as User. - Cuando usas claves condicionales (
null), el tipo de datos sigue siendoData | undefinedincluso con suspense. - El tipo
Keyaceptastring,any[],null,undefinedo() => Key. - Para comprobación estricta de null, gestiona siempre el caso
undefineddedataaunque esperes que ya esté cargado.
Errores comunes
res.json()devuelvePromise<any>por defecto. Debes hacer un cast:res.json() as Promise<User>. Es un límite de confianza: no se realiza validación en tiempo de ejecución.- Si el genérico del fetcher no coincide con el genérico de
useSWR, TypeScript puede ampliar silenciosamente aany. Comprueba siempre que tus tipos coincidan. erroresanypor defecto si no especificas el genérico Error. Especifícalo siempre para seguridad de tipos:useSWR<Data, Error>.- Al envolver
useSWRen un hook personalizado, el tipo de retorno esSWRResponse<Data, Error>. Impórtalo para tipado explícito. - Las firmas de tipo del middleware son complejas. Usa la exportación del tipo
Middlewareen lugar de escribir la firma completa manualmente.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Genéricos explícitos en useSWR | Control total, tipos claros | Verboso, alineación manual |
| Inferidos desde el fetcher | Menos boilerplate | Fácil devolver any por accidente |
| Hooks personalizados tipados | Encapsulados, reutilizables | Capa de abstracción adicional |
| Validación en tiempo de ejecución (zod) | Seguridad real en runtime | Dependencia extra, coste de rendimiento |
FAQs
¿Cómo paso genéricos a useSWR para obtener seguridad de tipos completa?
const { data, error } = useSWR<User, Error>("/api/me", fetcher);
// data: User | undefined
// error: Error | undefinedHasta tres genéricos: useSWR<Data, Error, Key>.
¿Puede SWR inferir el tipo de datos desde el fetcher sin genéricos explícitos?
Sí. Si tu fetcher tiene un retorno tipado, SWR infiere Data a partir de él:
const fetchUser = (url: string): Promise<User> =>
fetch(url).then((r) => r.json());
const { data } = useSWR("/api/me", fetchUser);
// data: User | undefined (inferido)Trampa común: ¿Por qué res.json() devuelve Promise<any> y cómo debo gestionarlo?
res.json() siempre devuelve Promise<any> en TypeScript. Debes hacer un cast: res.json() as Promise<User>. Es un límite de confianza: TypeScript no puede validar la forma en tiempo de ejecución. Considera usar Zod para validación en runtime.
¿Qué es el tipo Fetcher y cómo lo uso?
import { Fetcher } from "swr";
const fetcher: Fetcher<User[], string> = (url) =>
fetch(url).then((r) => r.json());Fetcher<Data, Key> exige que el fetcher acepte el tipo de clave y devuelva Promise<Data>.
¿Cómo tipifico useSWRMutation con los cuatro genéricos?
useSWRMutation<Data, Error, Key, Arg>(key, mutationFn);Data: tipo de retorno de la mutaciónError: tipo de errorKey: tipo de la clave de cachéArg: tipo del argumento pasado atrigger
Trampa común: ¿Qué ocurre si el genérico del fetcher no coincide con el de useSWR?
TypeScript puede ampliar silenciosamente a any en lugar de mostrar un error de compilación. Comprueba siempre que el tipo de retorno de tu fetcher coincida con el genérico Data que pasas a useSWR.
¿Cómo tipifico claves en array para useSWR?
type FetcherArgs = [url: string, token: string];
const fetcher: Fetcher<User, FetcherArgs> = ([url, token]) =>
fetch(url, {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
const { data } = useSWR<User, Error, FetcherArgs>(
["/api/me", authToken],
fetcher
);¿Qué es el tipo SWRResponse y cuándo debo usarlo?
Al envolver useSWR en un hook personalizado, el tipo de retorno es SWRResponse<Data, Error>. Impórtalo desde swr para tipar explícitamente el valor de retorno de tu hook personalizado.
¿Cómo tipifico una función middleware de SWR?
import { Middleware, SWRHook } from "swr";
const myMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, config);
};
};Usa la exportación del tipo Middleware en lugar de escribir la firma completa manualmente.
¿Debo usar genéricos explícitos o casts con as para tipar los datos de SWR?
Prefiere siempre genéricos explícitos (useSWR<User>) frente a casts con as (data as User). Los genéricos proporcionan seguridad en tiempo de compilación en todo el uso del hook, mientras que los casts solo silencian el comprobador de tipos en un punto.
¿Cómo tipifico el objeto SWRConfiguration para una variable de configuración?
import type { SWRConfiguration } from "swr";
const config: SWRConfiguration<User, Error> = {
revalidateOnFocus: false,
onSuccess: (data) => {
console.log(data.name); // data está tipado como User
},
};