How Jotai Hooks Use Function Overload in TypeScript

Although Function Overload Should Generally Be Avoided

Hi,

Today, let’s take a look at the Jotai code—specifically the types for useAtom:

export function useAtom<Value, Args extends unknown[], Result>(
  atom: WritableAtom<Value, Args, Result>,
  options?: Options,
): [Awaited<Value>, SetAtom<Args, Result>]

export function useAtom<Value>(
  atom: PrimitiveAtom<Value>,
  options?: Options,
): [Awaited<Value>, SetAtom<[SetStateAction<Value>], void>]

export function useAtom<Value>(
  atom: Atom<Value>,
  options?: Options,
): [Awaited<Value>, never]

export function useAtom<
  AtomType extends WritableAtom<unknown, never[], unknown>,
>(
  atom: AtomType,
  options?: Options,
): [
  Awaited<ExtractAtomValue<AtomType>>,
  SetAtom<ExtractAtomArgs<AtomType>, ExtractAtomResult<AtomType>>,
]

export function useAtom<AtomType extends Atom<unknown>>(
  atom: AtomType,
  options?: Options,
): [Awaited<ExtractAtomValue<AtomType>>, never]

Yeah, it's a bit much. Let's go through them one by one.

The most basic one is the third overload. It takes a read-only atom and returns a pair, where the second value is never. One important detail: the first value is wrapped in Awaited because useAtom internally uses the use hook, so it can handle async atoms.

So, when is the second value not never? That’s when the atom is writable. See the first overload—it takes a WritableAtom and returns a pair with the second value being a SetAtom function.

So far so good—but what about the second overload? It takes a PrimitiveAtom. This overload is technically unnecessary, but I added it because I noticed many people use useAtom<Type>(anAtom) to explicitly type primitive atoms. Without this overload, that kind of usage would fall through to the third one, which returns never for the setter. That would confuse users. Removing the explicit type annotation works, but this extra overload avoids the problem altogether.

Now the last two overloads are a bit tricky. They’re needed for more dynamic scenarios. For example:

const myAtom: Atom<number> | Atom<string> = Math.random() > 0.5 ? atom(0) : atom('')

const [value] = useAtom(myAtom)

In this case, we expect the value to be number | string. And it works—thanks to the last two overloads.
🔗 TypeScript playground example

If you remove the last two overloads, the myAtom in the useAtom call would cause a type error.

Technically, we could just keep the last two overloads. But doing that would make the more common use cases less ergonomic. So we ended up with five overloads to cover both typical and advanced usage.

I think this kind of overload usage is specific to library code. In application code, I generally prefer avoiding function overloads.

Happy coding.

Reply

or to participate.