Search code examples
reactjstypescripttanstackreact-queryhono

How to dynamically infer properties from a union type response in TypeScript?


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.


Solution

  • 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);
      }
    }