Alright, let’s settle a debate that’s less about technical superiority and more about developer ergonomics and how you like your data served: in a neatly wrapped object package or a structured tuple takeout box.

When you craft a custom hook, the most important contract you design is its return value. It’s the entire API for anyone using your hook. Get it wrong, and you’ll have a chorus of frustrated developers (or future-you) complaining. TypeScript gives you two primary ways to define this return type: an array/tuple or a plain object.

The Case for the Tuple

The tuple approach is what you see the big leagues use. useState returns a tuple: [state, setState]. So does useReducer. There’s a good reason for this: it enables immediate, arbitrary destructuring.

import { useState } from 'react';

// A simple hook that fetches data
function useDataFetcher<DataType>(url: string) {
  const [data, setData] = useState<DataType | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  // ... effect logic to fetch data ...

  return [data, loading, error];
}

// Usage: Clean, names-are-up-to-you destructuring
const [user, isLoading, fetchError] = useDataFetcher<User>('/api/user/123');

This is wonderfully concise. The hook doesn’t impose names on the consumer; the developer picking it up can call the destructured variables whatever they want. [data, loading, error] becomes [user, isUserLoading, userError] instantly.

But here’s the first rub. TypeScript, by default, is… not great at inferring tuple types. It sees return [data, loading, error]; and thinks, “Ah, an array! Its type is (DataType | null | boolean | Error)[].” That’s a useless union of all possible types, losing all positional information.

You have to force it to be a tuple. You can do this with a const assertion or an explicit return type.

// Option 1: "const" assertion (for simple cases)
return [data, loading, error] as const; // Type is read-only tuple: readonly [DataType | null, boolean, Error | null]

// Option 2: Explicit function return type (my preferred, more robust method)
function useDataFetcher<DataType>(url: string): [DataType | null, boolean, Error | null] {
  // ... logic ...
  return [data, loading, error]; // Now TS knows it's a tuple
}

The as const is clever but makes the tuple readonly, which can be annoying if you’re trying to destructure into already-declared mutable variables. The explicit return type is bulletproof and self-documenting.

The Case for the Object

Now, let’s look at the object return. Its superpower is explicit, safe, and name-stable destructuring.

function useDataFetcher<DataType>(url: string) {
  const [data, setData] = useState<DataType | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  // ... effect logic ...

  return { data, loading, error };
}

// Usage: Destructuring by name, order doesn't matter.
const { data: user, error: fetchError, loading: isPending } = useDataFetcher<User>('/api/user/123');

See the beauty here? The order is irrelevant. You can destructure just one property without a care in the world. const { loading } = useDataFetcher(...); works perfectly. TypeScript infers this return type perfectly without any extra help—it’s just an object. No fiddling with as const or manual types needed.

This approach avoids one of the biggest pitfalls of tuples: accidentally grabbing the wrong value by position. With an object, you cannot mistake loading for error. It’s self-documenting and far more resistant to errors if you later add a fourth value to the return; existing destructuring of the first three won’t break, whereas a tuple would completely shift the meaning of any subsequent values.

So, Which One Should You Use?

Here’s the rule of thumb I live by, forged in the fires of real-world use and debugging:

Use a tuple when your return values are a direct analog to useState—a value and a setter. This is the established pattern React itself uses, and developers’ brains are wired for it. A hook like useToggle is a prime candidate: const [isOn, toggle] = useToggle(true);. The tuple structure mirrors the built-in hooks it’s composed from.

Use an object for virtually everything else. Especially if you’re returning more than two values, or if the values are all related state (like the data, loading, error pattern). The safety, flexibility, and excellent TypeScript inference of the object approach make it the default choice for complex hooks. The only minor downside is slightly more verbose destructuring if you need all the properties, but that’s a tiny price to pay for not shipping a subtle positional bug.

Don’t just follow what you saw in one blog post. Think about the consumer of your hook—often your future self—and choose the API that is hardest to use incorrectly.