Lesson 28 · Module 4.5 — Library-grade patterns

Why Zustand writes create<State>()(...) — the double call

Every Zustand example has the same odd shape: create<State>()(...) — you pass the type, then call twice, with an empty () in the middle. It looks like a typo, but two separate TypeScript limits are stacked here. First, the state type T is un-inferable — it sits in both input and output positions of your store function — so you must annotate it by hand. Second, and this is what the empty () is for: TypeScript won't let you annotate one type parameter while inferring the others, so currying splits them — the first call annotates the state, the second infers the rest (the middleware).

The reading move

When you see a function called as fn<T>()(arg) — a generic + empty call + real call — read it as "annotate the type I must pin, then infer the rest from the argument". The first call locks the type you supply; the second call's argument is checked against it — and crucially, any other type parameters are still inferred from that argument, which one combined call can't do (TypeScript makes you pass all type arguments or none).

The inference problem, minimised

Strip Zustand down to its essence. The store function both returns the state and reads it back via get:

declare const create: <T>(f: (get: () => T) => T) => T;

const x = create((get) => ({
  foo: 0,
  bar: () => get(),
}));
// x is inferred as `unknown`, NOT { foo: number; bar: () => ... }

Why unknown? To infer T, TS matches the object you return against T. That object is { foo: 0, bar: () => get() }, and get() is typed T — so its type is { foo: number; bar: () => T }. Now TS is stuck solving T = { foo: number; bar: () => T }: T defined in terms of itself. The one piece that could pin it down — bar's return — is just T again, so the definition loops with no concrete value to start from. You never make a T here; you only read one back out through get and hand it along — so there's nothing for TS to read the type off of, and it falls back to unknown. (The jargon for "T sits in both an output and an input like this" is invariant, and invariant type parameters can't be inferred.)

The fix: annotate T — then curry to keep the rest inferred

The actual fix for that unknown is annotating the state type by hand, not the currying. In our minimal single-T example, even create<BearState>((get) => …) in one call would do it — you supplied T, so there's nothing left to infer.

So why the extra ()? Because the real create has more type parameters than the state alone — the middleware mutator types — and you want to annotate the state while letting those infer from your initializer. TypeScript won't allow that in one call: you pass all type arguments or none (microsoft/TypeScript#10571). Currying splits the two groups so each call's type-argument list is all-or-nothing on its own.

See it in miniature — a two-parameter function where you must write the first type but want the second inferred:

declare function build<State, Plugin>(
  f: (get: () => State) => State,   // State is only passed through, never made → can't be inferred (like T)
  plug: (s: State) => Plugin,        // Plugin is inferable from plug
): Plugin;

build(f, plug);            // ❌ State can't be inferred → unknown
build<MyState>(f, plug);   // ❌ "Expected 2 type arguments, but got 1" — you'd have to write Plugin too

One call traps you: infer both or write both, never mix. Curry it — give each function its own type-parameter list — and the deadlock breaks:

declare function build<State>(): <Plugin>(
  f: (get: () => State) => State,
  plug: (s: State) => Plugin,
) => Plugin;

build<MyState>()(f, plug);   // ✅ State annotated in call 1, Plugin inferred in call 2

Zustand's real create is this exact shape — the state annotated up front, the middleware mutators inferred from your initializer:

// shape of the curried create (simplified):
declare const create:
  <T>() => (f: StateCreator<T>) => UseBoundStore<StoreApi<T>>;

// so you call it in two steps:
interface BearState { bears: number; increase: (by: number) => void; }

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((s) => ({ bears: s.bears + by })),
}));
The empty () is the first call — it pins T = BearState while leaving the middleware types to be inferred from your (set) => ({...}) in the second call. By the time that creator is checked, T is fixed, so TS just verifies your object matches BearState and types set for you.

Reading StateCreator<T>

The argument type is StateCreator. Read it as "a function you write that receives set and get and returns the initial state":

// simplified — the middleware mutator params omitted
type StateCreator<T> = (
  set: (partial: Partial<T> | ((s: T) => Partial<T>)) => void,
  get: () => T,
) => T;

With T already locked to BearState, every piece resolves: set accepts a partial BearState, get() returns a full BearState, and your returned object must be a BearState. The invariance that broke single-call inference is now harmless — you supplied T, so nothing has to be inferred from a position that depends on itself.

Why not just one call?

For a lone state type, create<BearState>((set) => …) would be enough — annotating T is the real fix. The empty () earns its keep the moment you add middleware (persist, immer, devtools): each brings extra type parameters you want inferred, and TypeScript can't annotate the state while inferring those in one call. The () is what keeps the middleware types inferred — that's the feature, not noise.

Check yourself

Recall, don't re-read. (Answers reveal on click.)

Why does single-call create((get) => …) infer the state as unknown?
What is the empty () in create<State>()(...) doing?
Once T = BearState is fixed, what does the StateCreator<T> argument require?

Primary source

The reasoning is laid out in Zustand's own TypeScript guide (see “Why can't we just infer the type from the initial state?” — the invariance explanation). Best way to feel it: paste the minimal declare const create example into the TS Playground, hover x to see unknown, then add a leading <T>() currying step and watch the type resolve.

Try it yourself

See a generic called as fn<T>()(arg) anywhere and unsure why? Paste it and ask “is this currying so one type param can be annotated while the rest still infer?” Next lesson (4.6) is the capstone — it pulls every mental model from Modules 0–4 together: types are the new regex.