From 339e8d3f5c4a37a595c41f6fc2e49ce933d67523 Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Mon, 11 May 2026 15:56:59 +0100 Subject: [PATCH 1/2] add typed custom produce function --- website/docs/produce.mdx | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/website/docs/produce.mdx b/website/docs/produce.mdx index b1747689..ec55f22b 100644 --- a/website/docs/produce.mdx +++ b/website/docs/produce.mdx @@ -81,6 +81,67 @@ expect(nextState[0]).toBe(baseState[0]) expect(nextState[1]).not.toBe(baseState[1]) ``` +## TypeScript example + +If you want to pass a typed recipe callback around, you can type it using `Draft` and allow either `void` or a replacement state as the return value. + +```ts +import {produce} from "immer" +import type {Draft} from "immer" + +type Recipe = (draft: Draft) => void | S + +function updateState( + setState: (updater: (prev: S) => S) => void, + recipe: Recipe +) { + setState(prev => produce(prev, recipe)) +} +``` + +## React example + +A common usage in React is to wrap `setState` with `produce` so callers can provide a typed recipe. + +```tsx +import {produce} from "immer" +import type {Draft} from "immer" +import {useCallback, useState} from "react" + +type State = { + foo: string + bar: string +} + +const initialState: State = { + foo: "", + bar: "" +} + +type Recipe = (draft: Draft) => void | State + +export function Example() { + const [state, setState] = useState(initialState) + + const updateState = useCallback((recipe: Recipe) => { + setState(prev => produce(prev, recipe)) + }, []) + + return ( + + ) +} +``` + ### Terminology - `(base)state`, the immutable state passed to `produce` From 420e5ffed9137660d029644e3c57f053fbc0541c Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Tue, 28 Jul 2026 16:24:04 +0100 Subject: [PATCH 2/2] Move TypeScript recipe patterns to TypeScript docs including crosslink --- website/docs/produce.mdx | 64 ++----------------------------------- website/docs/typescript.mdx | 64 +++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/website/docs/produce.mdx b/website/docs/produce.mdx index ec55f22b..cdd23935 100644 --- a/website/docs/produce.mdx +++ b/website/docs/produce.mdx @@ -9,8 +9,7 @@ title: Using produce data-ea-type="image" className="horizontal bordered" > - -
+
egghead.io lesson 3: Simplifying deep updates with _produce_ @@ -81,66 +80,7 @@ expect(nextState[0]).toBe(baseState[0]) expect(nextState[1]).not.toBe(baseState[1]) ``` -## TypeScript example - -If you want to pass a typed recipe callback around, you can type it using `Draft` and allow either `void` or a replacement state as the return value. - -```ts -import {produce} from "immer" -import type {Draft} from "immer" - -type Recipe = (draft: Draft) => void | S - -function updateState( - setState: (updater: (prev: S) => S) => void, - recipe: Recipe -) { - setState(prev => produce(prev, recipe)) -} -``` - -## React example - -A common usage in React is to wrap `setState` with `produce` so callers can provide a typed recipe. - -```tsx -import {produce} from "immer" -import type {Draft} from "immer" -import {useCallback, useState} from "react" - -type State = { - foo: string - bar: string -} - -const initialState: State = { - foo: "", - bar: "" -} - -type Recipe = (draft: Draft) => void | State - -export function Example() { - const [state, setState] = useState(initialState) - - const updateState = useCallback((recipe: Recipe) => { - setState(prev => produce(prev, recipe)) - }, []) - - return ( - - ) -} -``` +For advanced TypeScript typing scenarios, see [Using TypeScript or Flow](./typescript.mdx). ### Terminology diff --git a/website/docs/typescript.mdx b/website/docs/typescript.mdx index cdfd7316..73d227d3 100644 --- a/website/docs/typescript.mdx +++ b/website/docs/typescript.mdx @@ -10,8 +10,7 @@ sidebar_label: TypeScript / Flow data-ea-type="image" className="horizontal bordered" > - -
+
egghead.io lesson 12: Immer + TypeScript @@ -64,6 +63,67 @@ This ensures that the only place you can modify your state is in your produce ca 2. You can use the utility type `Immutable` to recursively make an entire type tree read-only, e.g.: `type ReadonlyState = Immutable`. 3. Immer won't automatically wrap all returned types in `Immutable` if the original type of the input state wasn't immutable. This is to make sure it doesn't break code bases that don't use immutable types. +## Typing reusable recipes + +If you want to pass a recipe callback around, you can type it using `Draft` and allow either `void` or a replacement state as the return value. + +```ts +import {produce} from "immer" +import type {Draft} from "immer" + +type Recipe = (draft: Draft) => void | S + +function updateState( + setState: (updater: (prev: S) => S) => void, + recipe: Recipe +) { + setState(prev => produce(prev, recipe)) +} +``` + +### React example + +A common usage in React is to wrap `setState` with `produce` so callers can provide a typed recipe. + +```tsx +import {produce} from "immer" +import type {Draft} from "immer" +import {useCallback, useState} from "react" + +type State = { + foo: string + bar: string +} + +const initialState: State = { + foo: "", + bar: "" +} + +type Recipe = (draft: Draft) => void | State + +export function Example() { + const [state, setState] = useState(initialState) + + const updateState = useCallback((recipe: Recipe) => { + setState(prev => produce(prev, recipe)) + }, []) + + return ( + + ) +} +``` + ## Tips for curried producers We try to inference as much as possible. So if a curried producer is created and directly passed to another function, we can infer the type from there. This works well with for example React: