I am working with a TypeScript project where I need to handle responses from an API that can return different structures based on success or error cases. The responses are typed as a union, and I want to dynamically check for properties without hardcoding their names.
I am using React Query’s useMutation for handling the login process. Here’s how I’ve set up my hook:
import { useMutation } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { InferRequestType, InferResponseType } from "hono";
import { useNavigate } from "@tanstack/react-router";
const login = api.auth.login.$post;
type LoginRequest = InferRequestType<typeof login>;
type LoginResponse = InferResponseType<typeof login>;
const loginFunction = async (
credentials: LoginRequest
): Promise<LoginResponse> => {
const res = await login(credentials);
if (!res.ok) throw new Error("Failed to login");
const json = await res.json();
return json;
};
export const useLoginMutation = () => {
const navigate = useNavigate();
const mutation = useMutation<LoginResponse, Error, LoginRequest>({
mutationFn: loginFunction,
mutationKey: ["auth", "login"],
onSuccess: (data) => {},
onError: (error) => {},
});
return mutation;
};
That's how LoginResponse is inferred:
type LoginResponse = {
success: boolean;
message: string;
} | {
success: boolean;
message: string;
} | {
success: boolean;
message: string;
accessToken: string;
}
When response has status code 401 or 403, i get success and message. When 201: success, message, accessToken.
While using the onSuccess handler in the useMutation hook, I want TypeScript to understand that data has an accessToken without explicitly hardcoding property names. 401 or 403 are triggering Error in loginFunction, which then triggers onError in useMutation.
to handle different response structures dynamically you can use type guards to help TypeScript understand which type of the union is being used. A type guard is a function that checks if an object is of a certain type.
here's an example:
function hasAccessToken(response: LoginResponse): response is { success: boolean; message: string; accessToken: string } {
return 'accessToken' in response;
}
You can use this type guard in your onSuccess handler.
something like this so you're not hardcoding the property names...
onSuccess: (data) => {
if (hasAccessToken(data)) {
// TypeScript now knows that data has an accessToken property
console.log(data.accessToken);
}
}