- Daishi Kato's Read the Code
- Posts
- How Jotai Hooks Use Function Overload in TypeScript
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