Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Type your useSWR calls with generics to get full type safety on data, error, keys, and fetcher functions. SWR's TypeScript support enables inference of return types from fetcher signatures.
"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>; // data is User here
}
}"use client";
import useSWR, { Fetcher, SWRConfiguration, Key } from "swr";
import useSWRMutation, { SWRMutationConfiguration } from "swr/mutation";
// ---- Type-safe fetcher factory ----
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>;
};
}
// ---- Domain types ----
interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface CreateProductInput {
name: string;
price: number;
category: string;
}
// ---- Type-safe hooks ----
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[]>());
}
// ---- Type-safe mutation ----
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
);
}
// ---- Usage ----
export default function ProductPage({ id }: { id: string }) {
const { data: product, error, isLoading } = useProduct(id);
const { trigger, isMutating } = useCreateProduct();
if (isLoading) return <div>Loading...</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}
>
Add Product
</button>
</div>
);
}useSWR<Data, Error, Key> accepts up to three generic parameters: the data type, the error type, and the key type.Data generic from the fetcher's return type.Fetcher<Data, Key> type from SWR enforces that the fetcher accepts the key type and returns Promise<Data>.useSWRMutation<Data, Error, Key, Arg> adds a fourth generic for the mutation argument type.SWRResponse, SWRConfiguration, Key, and Fetcher for advanced typing.Inferred types from fetcher:
// SWR infers Data from the fetcher return type
const fetchUser = (url: string): Promise<User> => fetch(url).then((r) => r.json());
const { data } = useSWR("/api/me", fetchUser);
// data: User | undefined - inferred from fetchUserArray key typing:
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
);Typed middleware:
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;
};
};Typed SWR configuration:
import type { SWRConfiguration } from "swr";
const defaultConfig: SWRConfiguration<User, Error> = {
revalidateOnFocus: false,
errorRetryCount: 3,
onSuccess: (data) => {
// data is typed as User
console.log(data.name);
},
};as casts for type safety. useSWR<User> is better than data as User.null), the data type remains Data | undefined even with suspense.Key type accepts string, any[], null, undefined, or () => Key.undefined case for data even when you expect it to be loaded.res.json() returns Promise<any> by default. You must cast it: res.json() as Promise<User>. This is a trust boundary - runtime validation is not performed.useSWR generic, TypeScript may silently widen to any. Always verify your types align.error defaults to any if you do not specify the Error generic. Always specify it for type safety: useSWR<Data, Error>.useSWR in a custom hook, the return type is SWRResponse<Data, Error>. Import it for explicit typing.Middleware type export rather than writing the full signature manually.| Approach | Pros | Cons |
|---|---|---|
| Explicit generics on useSWR | Full control, clear types | Verbose, manual alignment |
| Inferred from fetcher | Less boilerplate | Easy to accidentally return any |
| Custom typed hooks | Encapsulated, reusable | Extra abstraction layer |
| Runtime validation (zod) | True runtime safety | Extra dependency, performance cost |
const { data, error } = useSWR<User, Error>("/api/me", fetcher);
// data: User | undefined
// error: Error | undefinedUp to three generics: useSWR<Data, Error, Key>.
Yes. If your fetcher has a typed return, SWR infers Data from it:
const fetchUser = (url: string): Promise<User> =>
fetch(url).then((r) => r.json());
const { data } = useSWR("/api/me", fetchUser);
// data: User | undefined (inferred)res.json() always returns Promise<any> in TypeScript. You must cast it: res.json() as Promise<User>. This is a trust boundary -- TypeScript cannot validate the runtime shape. Consider using Zod for runtime validation.
import { Fetcher } from "swr";
const fetcher: Fetcher<User[], string> = (url) =>
fetch(url).then((r) => r.json());Fetcher<Data, Key> enforces that the fetcher accepts the key type and returns Promise<Data>.
useSWRMutation<Data, Error, Key, Arg>(key, mutationFn);Data: return type of the mutationError: error typeKey: the cache key typeArg: the argument type passed to triggerTypeScript may silently widen to any instead of showing a compile error. Always verify that your fetcher's return type aligns with the Data generic you pass to 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
);When wrapping useSWR in a custom hook, the return type is SWRResponse<Data, Error>. Import it from swr for explicit typing of your custom hook's return value.
import { Middleware, SWRHook } from "swr";
const myMiddleware: Middleware = (useSWRNext: SWRHook) => {
return (key, fetcher, config) => {
return useSWRNext(key, fetcher, config);
};
};Use the Middleware type export rather than writing the full signature manually.
Always prefer explicit generics (useSWR<User>) over as casts (data as User). Generics provide compile-time safety across the entire hook usage, while casts only silence the type checker at one point.
import type { SWRConfiguration } from "swr";
const config: SWRConfiguration<User, Error> = {
revalidateOnFocus: false,
onSuccess: (data) => {
console.log(data.name); // data is typed as User
},
};Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥