TypeScript with SWR
Recipe
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
}
}Working Example
"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>
);
}Deep Dive
How It Works
useSWR<Data, Error, Key>accepts up to three generic parameters: the data type, the error type, and the key type.- When you provide a typed fetcher, SWR can infer the
Datageneric from the fetcher's return type. - The
Fetcher<Data, Key>type from SWR enforces that the fetcher accepts the key type and returnsPromise<Data>. useSWRMutation<Data, Error, Key, Arg>adds a fourth generic for the mutation argument type.- SWR exports utility types like
SWRResponse,SWRConfiguration,Key, andFetcherfor advanced typing.
Variations
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);
},
};TypeScript Notes
- Always prefer explicit generics over
ascasts for type safety.useSWR<User>is better thandata as User. - When using conditional keys (
null), the data type remainsData | undefinedeven with suspense. - The
Keytype acceptsstring,any[],null,undefined, or() => Key. - For strict null checking, always handle the
undefinedcase fordataeven when you expect it to be loaded.
Gotchas
res.json()returnsPromise<any>by default. You must cast it:res.json() as Promise<User>. This is a trust boundary — runtime validation is not performed.- If your fetcher generic does not match the
useSWRgeneric, TypeScript may silently widen toany. Always verify your types align. errordefaults toanyif you do not specify the Error generic. Always specify it for type safety:useSWR<Data, Error>.- When wrapping
useSWRin a custom hook, the return type isSWRResponse<Data, Error>. Import it for explicit typing. - Middleware type signatures are complex. Use the
Middlewaretype export rather than writing the full signature manually.
Alternatives
| 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 |
FAQs
How do I pass generics to useSWR for full type safety?
const { data, error } = useSWR<User, Error>("/api/me", fetcher);
// data: User | undefined
// error: Error | undefinedUp to three generics: useSWR<Data, Error, Key>.
Can SWR infer the data type from the fetcher without explicit generics?
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)Gotcha: Why does res.json() return Promise<any> and how should I handle it?
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.
What is the Fetcher type and how do I use it?
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>.
How do I type useSWRMutation with all four generics?
useSWRMutation<Data, Error, Key, Arg>(key, mutationFn);Data: return type of the mutationError: error typeKey: the cache key typeArg: the argument type passed totrigger
Gotcha: What happens if the fetcher generic does not match the useSWR generic?
TypeScript 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.
How do I type array keys for 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
);What is the SWRResponse type and when should I use it?
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.
How do I type a SWR middleware function?
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.
Should I use explicit generics or as-casts for typing SWR data?
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.
How do I type the SWRConfiguration object for a config variable?
import type { SWRConfiguration } from "swr";
const config: SWRConfiguration<User, Error> = {
revalidateOnFocus: false,
onSuccess: (data) => {
console.log(data.name); // data is typed as User
},
};