Repository: immerjs/immer
Pull request #770 (2021-03-18 21:52:22)

Better types


Patch:
diff --git a/package.json b/package.json
index 21a969c..109dd05 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "immer",
-  "version": "7.0.0-beta.0",
+  "version": "9.0.0-beta.1",
   "description": "Create your next immutable state by mutating the current one",
   "main": "dist/index.js",
   "module": "dist/immer.esm.js",
@@ -92,7 +92,7 @@
     "spec.ts": "^1.1.0",
     "ts-jest": "^25.2.0",
     "tsdx": "^0.12.3",
-    "typescript": "^3.9.3",
+    "typescript": "^4.2.3",
     "webpack": "^4.41.6",
     "webpack-cli": "^3.3.11"
   }
diff --git a/src/types/types-external.ts b/src/types/types-external.ts
index 53a73af..c8ef906 100644
--- a/src/types/types-external.ts
+++ b/src/types/types-external.ts
@@ -1,11 +1,6 @@
 import {Nothing} from "../internal"
 
-type Tail<T extends any[]> = ((...t: T) => any) extends (
-	_: any,
-	...tail: infer TT
-) => any
-	? TT
-	: []
+type AnyFunc = (...args: any[]) => any
 
 type PrimitiveType = number | string | boolean
 
@@ -84,6 +79,79 @@ export type Produced<Base, Return> = Return extends void
 	? Promise<Result extends void ? Base : FromNothing<Result>>
 	: FromNothing<Return>
 
+/**
+ * Utility types
+ */
+type PatchesTuple<T> = readonly [T, Patch[], Patch[]]
+
+type ValidRecipeReturnType<State> =
+	| State
+	| void
+	| undefined
+	| (State extends undefined ? Nothing : never)
+
+type ValidRecipeReturnTypePossiblyPromise<State> =
+	| ValidRecipeReturnType<State>
+	| Promise<ValidRecipeReturnType<State>>
+
+type PromisifyReturnIfNeeded<
+	State,
+	Recipe extends AnyFunc,
+	UsePatches extends boolean
+> = ReturnType<Recipe> extends Promise<any>
+	? Promise<UsePatches extends true ? PatchesTuple<State> : State>
+	: UsePatches extends true
+	? PatchesTuple<State>
+	: State
+
+/**
+ * Core Producer inference
+ */
+type InferRecipeFromCurried<Curried> = Curried extends (
+	base: infer State,
+	...rest: infer Args
+) => any // extra assertion to make sure this is a proper curried function (state, args) => state
+	? ReturnType<Curried> extends State
+		? (
+				draft: Draft<State>,
+				...rest: Args
+		  ) => ValidRecipeReturnType<Draft<State>>
+		: never
+	: never
+
+type InferInitialStateFromCurried<Curried> = Curried extends (
+	base: infer State,
+	...rest: any[]
+) => any // extra assertion to make sure this is a proper curried function (state, args) => state
+	? State
+	: never
+
+type InferCurriedFromRecipe<
+	Recipe,
+	UsePatches extends boolean
+> = Recipe extends (draft: infer DraftState, ...args: infer RestArgs) => any // verify return type
+	? ReturnType<Recipe> extends ValidRecipeReturnTypePossiblyPromise<DraftState>
+		? (
+				base: Immutable<DraftState>,
+				...args: RestArgs
+		  ) => PromisifyReturnIfNeeded<DraftState, Recipe, UsePatches> // N.b. we return mutable draftstate, in case the recipe's first arg isn't read only, and that isn't expected as output either
+		: never // incorrect return type
+	: never // not a function
+
+type InferCurriedFromInitialStateAndRecipe<
+	State,
+	Recipe,
+	UsePatches extends boolean
+> = Recipe extends (
+	draft: Draft<State>,
+	...rest: infer RestArgs
+) => ValidRecipeReturnTypePossiblyPromise<State>
+	? (
+			base?: State | undefined,
+			...args: RestArgs
+	  ) => PromisifyReturnIfNeeded<State, Recipe, UsePatches>
+	: never // recipe doesn't match initial state
+
 /**
  * The `produce` function takes a value and a "recipe function" (whose
  * return value often depends on the base state). The recipe function is
@@ -104,40 +172,58 @@ export type Produced<Base, Return> = Return extends void
  * @returns {any} a new state, or the initial state if nothing was modified
  */
 export interface IProduce {
-	/** Curried producer */
-	<
-		Recipe extends (...args: any[]) => any,
-		Params extends any[] = Parameters<Recipe>,
-		T = Params[0]
-	>(
-		recipe: Recipe
-	): <Base extends Immutable<T>>(
-		base: Base,
-		...rest: Tail<Params>
-	) => Produced<Base, ReturnType<Recipe>>
-	//   ^ by making the returned type generic, the actual type of the passed in object is preferred
-	//     over the type used in the recipe. However, it does have to satisfy the immutable version used in the recipe
-	//     Note: the type of S is the widened version of T, so it can have more props than T, but that is technically actually correct!
-
-	/** Curried producer with initial state */
-	<
-		Recipe extends (...args: any[]) => any,
-		Params extends any[] = Parameters<Recipe>,
-		T = Params[0]
-	>(
+	/** Curried producer that infers the recipe from the curried output function (e.g. when passing to setState) */
+	<Curried>(
+		recipe: InferRecipeFromCurried<Curried>,
+		initialState?: InferInitialStateFromCurried<Curried>
+	): Curried
+
+	/** Curried producer that infers curried from the recipe  */
+	<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<
+		Recipe,
+		false
+	>
+
+	/** Curried producer that infers curried from the State generic, which is explicitly passed in.  */
+	<State>(
+		recipe: (
+			state: Draft<State>,
+			initialState: State
+		) => ValidRecipeReturnType<State>
+	): (state?: State) => State
+	<State, Args extends any[]>(
+		recipe: (
+			state: Draft<State>,
+			...args: Args
+		) => ValidRecipeReturnType<State>,
+		initialState: State
+	): (state?: State, ...args: Args) => State
+	<State>(recipe: (state: Draft<State>) => ValidRecipeReturnType<State>): (
+		state: State
+	) => State
+	<State, Args extends any[]>(
+		recipe: (state: Draft<State>, ...args: Args) => ValidRecipeReturnType<State>
+	): (state: State, ...args: Args) => State
+
+	/** Curried producer with initial state, infers recipe from initial state */
+	<State, Recipe extends Function>(
 		recipe: Recipe,
-		initialState: Immutable<T>
-	): <Base extends Immutable<T>>(
-		base?: Base,
-		...rest: Tail<Params>
-	) => Produced<Base, ReturnType<Recipe>>
+		initialState: State
+	): InferCurriedFromInitialStateAndRecipe<State, Recipe, false>
+
+	/** Promisified dormal producer */
+	<Base, D = Draft<Base>>(
+		base: Base,
+		recipe: (draft: D) => Promise<ValidRecipeReturnType<D>>,
+		listener?: PatchListener
+	): Promise<Base>
 
 	/** Normal producer */
-	<Base, D = Draft<Base>, Return = void>(
+	<Base, D = Draft<Base>>( // By using a default inferred D, rather than Draft<Base> in the recipe, we can override it.
 		base: Base,
-		recipe: (draft: D) => Return,
+		recipe: (draft: D) => ValidRecipeReturnType<D>,
 		listener?: PatchListener
-	): Produced<Base, Return>
+	): Base
 }
 
 /**
@@ -147,39 +233,22 @@ export interface IProduce {
  * Like produce, this function supports currying
  */
 export interface IProduceWithPatches {
-	/** Curried producer */
-	<
-		Recipe extends (...args: any[]) => any,
-		Params extends any[] = Parameters<Recipe>,
-		T = Params[0]
-	>(
-		recipe: Recipe
-	): <Base extends Immutable<T>>(
-		base: Base,
-		...rest: Tail<Params>
-	) => [Produced<Base, ReturnType<Recipe>>, Patch[], Patch[]]
-	//   ^ by making the returned type generic, the actual type of the passed in object is preferred
-	//     over the type used in the recipe. However, it does have to satisfy the immutable version used in the recipe
-	//     Note: the type of S is the widened version of T, so it can have more props than T, but that is technically actually correct!
-
-	/** Curried producer with initial state */
-	<
-		Recipe extends (...args: any[]) => any,
-		Params extends any[] = Parameters<Recipe>,
-		T = Params[0]
-	>(
+	// Types copied from IProduce, wrapped with PatchesTuple
+	<Recipe extends AnyFunc>(recipe: Recipe): InferCurriedFromRecipe<Recipe, true>
+	<State, Recipe extends Function>(
 		recipe: Recipe,
-		initialState: Immutable<T>
-	): <Base extends Immutable<T>>(
-		base?: Base,
-		...rest: Tail<Params>
-	) => [Produced<Base, ReturnType<Recipe>>, Patch[], Patch[]]
-
-	/** Normal producer */
-	<Base, D = Draft<Base>, Return = void>(
+		initialState: State
+	): InferCurriedFromInitialStateAndRecipe<State, Recipe, true>
+	<Base, D = Draft<Base>>(
 		base: Base,
-		recipe: (draft: D) => Return
-	): [Produced<Base, Return>, Patch[], Patch[]]
+		recipe: (draft: D) => ValidRecipeReturnType<Base>,
+		listener?: PatchListener
+	): PatchesTuple<Immutable<Base>>
+	<Base, D = Draft<Base>>(
+		base: Base,
+		recipe: (draft: D) => Promise<ValidRecipeReturnType<Base>>,
+		listener?: PatchListener
+	): PatchesTuple<Promise<Immutable<Base>>>
 }
 
 // Fixes #507: bili doesn't export the types of this file if there is no actual source in it..
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 58de2b6..16f9430 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -36,20 +36,11 @@ export function isDraftable(value: any): boolean {
 	)
 }
 
-const objectCtorString = Object.prototype.constructor.toString()
 /*#__PURE__*/
 export function isPlainObject(value: any): boolean {
 	if (!value || typeof value !== "object") return false
 	const proto = Object.getPrototypeOf(value)
-	if (proto === null) {
-		return true
-	}
-	const Ctor =
-		Object.hasOwnProperty.call(proto, "constructor") && proto.constructor
-	return (
-		typeof Ctor == "function" &&
-		Function.toString.call(Ctor) === objectCtorString
-	)
+	return !proto || proto === Object.prototype
 }
 
 /** Get the underlying object that is represented by the given draft */
diff --git a/website/docs/example-reducer.mdx b/website/docs/example-reducer.mdx
index 1f3d14d..58378fd 100644
--- a/website/docs/example-reducer.mdx
+++ b/website/docs/example-reducer.mdx
@@ -67,7 +67,6 @@ const byId = produce((draft, action) => {
 			action.products.forEach(product => {
 				draft[product.id] = product
 			})
-			break
 	}
 }, INITIAL_STATE)
 ```
diff --git a/website/docs/return.mdx b/website/docs/return.mdx
index d1c7e55..117c12b 100644
--- a/website/docs/return.mdx
+++ b/website/docs/return.mdx
@@ -9,7 +9,8 @@ title: Returning new data from producers
 		data-ea-type="image"
 		className="horizontal bordered"
 	></div>
-</center> <details>
+</center>
+<details>
 	<summary className="egghead-summary">
 		egghead.io lesson 9: Returning completely new state
 	</summary>
@@ -111,6 +112,8 @@ produce(state, draft => nothing)
 
 N.B. Note that this problem is specific for the `undefined` value, any other value, including `null`, doesn't suffer from this issue.
 
+Tip: to be able to return `nothing` from a recipe when using TypeScript, the `state`'s type must accept `undefined` as value.
+
 ## Inline shortcuts using `void`
 
 <details>
diff --git a/website/docs/typescript.mdx b/website/docs/typescript.mdx
index fd7e2cc..d24fa91 100644
--- a/website/docs/typescript.mdx
+++ b/website/docs/typescript.mdx
@@ -10,7 +10,8 @@ sidebar_label: TypeScript / Flow
 		data-ea-type="image"
 		className="horizontal bordered"
 	></div>
-</center> <details>
+</center>
+<details>
 	<summary className="egghead-summary">
 		egghead.io lesson 12: Immer + TypeScript
 	</summary>
@@ -55,32 +56,96 @@ const newState = produce(state, draft => {
 // `newState.x` cannot be modified here
 ```
 
-This ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with `ReadonlyArray`!
+This ensures that the only place you can modify your state is in your produce callbacks. It even works recursively and with `ReadonlyArray`.
 
-For curried reducers, the type is inferred from the first argument of recipe function, so make sure to type it. The `Draft` utility type can be used if the state argument type is immutable:
+## Best practices
 
-```ts
-import produce, {Draft} from "immer"
+1. Always define your states as `readonly` as much as possible. This best reflects the mental model and reality, since Immer will freeze all its returned values.
+2. You can use the utility type `Immutable` to recursively make an entire type tree read-only, e.g.: `type ReadonlyState = Immutable<State>`.
+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.
 
-interface State {
-	readonly x: number
-}
+## Tips for curried producers
 
-// `x` cannot be modified here
-const state: State = {
-	x: 0
-}
+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 will with for example React:
 
-const increment = produce((draft: Draft<State>, inc: number) => {
-	// `x` can be modified here
-	draft.x += inc
+```typescript
+import {Immutable, produce} from "immer"
+
+type Todo = Immutable<{
+	title: string
+	done: boolean
+}>
+
+// later...
+
+const [todo, setTodo] = useState<Todo>({
+	title: "test",
+	done: true
 })
 
-const newState = increment(state, 2)
-// `newState.x` cannot be modified here
+// later...
+
+setTodo(
+	produce(draft => {
+		// draft will be strongly typed and mutable!
+		draft.done = !draft.done
+	})
+)
+```
+
+When a curried producer isn't passed directly somewhere else, Immer can infer the state type from the draft argument. For example when doing the following:
+
+```typescript
+// See below for a better solution!
+
+const toggler = produce((draft: Draft<Todo>) => {
+	draft.done = !draft.done
+})
+
+// typeof toggler = (state: Immutable<Todo>) => Writable<Todo>
+```
+
+Note that we did wrap the `Todo` type of the `draft` argument with `Draft`, because `Todo` is a readonly type. For non-readonly types this isn't needed.
+
+For the returned curried function, `toggler`, We will _narrow_ the _input_ type to `Immutable<Todo>`, so that even though `Todo` is a mutable type, we will still accept an immutable todo as input argument to `toggler`.
+
+In contrast, Immer will _widen_ the _output_ type of the curried function to `Writable<Todo>`, to make sure it's output state is also assignable to variables that are not explictly typed to be immutable.
+
+This type narrowing / widening behavior might be unwelcome, maybe even for the simpe reason that it results in quite noisy types. So we recommend to specify the generic state type for curried producers instead, in cases where it cannot be inferred directly, like `toggler` above. By doing so the automatic output widening / input narrowing will be skipped. However, the `draft` argument itself will still be inferred to be a writable `Draft<Todo>`:
+
+```typescript
+const toggler = produce<Todo>(draft => {
+	draft.done = !draft.done
+})
+
+// typeof toggler = (state: Todo) => Todo
+```
+
+However, in case the curried producer is defined with an initial state, Immer can infer the state type from the initial state, so in that case the generic doesn't need to be specified either:
+
+```typescript
+const state0: Todo = {
+	title: "test",
+	done: false
+}
+
+// No type annotations needed, since we can infer from state0.
+const toggler = produce(draft => {
+	draft.done = !draft.done
+}, state0)
+
+// typeof toggler = (state: Todo) => Todo
 ```
 
-_Note: Since TypeScript support for recursive types is limited, and there is no co- contravariance, it might the easiest to not type your state as `readonly` (Immer will still protect against accidental mutations)_
+In case the toggler has no initial state, and it has curried arguments, and you set the state generic explicitly, then type of any additional arguments should be defined explicitly as a tuple type as well:
+
+```typescript
+const toggler = produce<Todo, [boolean]>((draft, newState) => {
+	draft.done = newState
+})
+
+// typeof toggler = (state: Todo, newState: boolean) => Todo
+```
 
 ## Cast utilities
 
diff --git a/yarn.lock b/yarn.lock
index 5c91e70..7e52ed0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8503,7 +8503,6 @@ npm@^6.10.3:
     cmd-shim "^3.0.3"
     columnify "~1.5.4"
     config-chain "^1.1.12"
-    debuglog "*"
     detect-indent "~5.0.0"
     detect-newline "^2.1.0"
     dezalgo "~1.0.3"
@@ -8518,7 +8517,6 @@ npm@^6.10.3:
     has-unicode "~2.0.1"
     hosted-git-info "^2.8.8"
     iferr "^1.0.2"
-    imurmurhash "*"
     infer-owner "^1.0.4"
     inflight "~1.0.6"
     inherits "^2.0.4"
@@ -8537,14 +8535,8 @@ npm@^6.10.3:
     libnpx "^10.2.4"
     lock-verify "^2.1.0"
     lockfile "^1.0.4"
-    lodash._baseindexof "*"
     lodash._baseuniq "~4.6.0"
-    lodash._bindcallback "*"
-    lodash._cacheindexof "*"
-    lodash._createcache "*"
-    lodash._getnative "*"
     lodash.clonedeep "~4.5.0"
-    lodash.restparam "*"
     lodash.union "~4.6.0"
     lodash.uniq "~4.5.0"
     lodash.without "~4.4.0"
@@ -11748,10 +11740,10 @@ typescript@^3.7.3:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
   integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
 
-typescript@^3.9.3:
-  version "3.9.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a"
-  integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==
+typescript@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
+  integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
 
 uglify-js@^3.1.4:
   version "3.7.7"


Test patch:
diff --git a/__tests__/produce.ts b/__tests__/produce.ts
index c0fb74e..efc4370 100644
--- a/__tests__/produce.ts
+++ b/__tests__/produce.ts
@@ -48,7 +48,7 @@ const expectedState: State = {
 }
 
 it("can update readonly state via standard api", () => {
-	const newState = produce<State>(state, draft => {
+	const newState = produce(state, draft => {
 		draft.num++
 		draft.foo = "bar"
 		draft.bar = "foo"
@@ -59,37 +59,48 @@ it("can update readonly state via standard api", () => {
 		draft.arr2[0].value = "foo"
 		draft.arr2.push({value: "asf"})
 	})
-	assert(newState, state)
+	assert(newState, _ as State)
 })
 
 // NOTE: only when the function type is inferred
 it("can infer state type from default state", () => {
 	type State = {readonly a: number}
-	type Recipe = <S extends State>(state?: S | undefined) => S
+	type Recipe = (state?: State | undefined) => State
 
 	let foo = produce((_: any) => {}, _ as State)
 	assert(foo, _ as Recipe)
 })
 
 it("can infer state type from recipe function", () => {
-	type State = {readonly a: string} | {readonly b: string}
-	type Recipe = <S extends State>(state: S) => S
-
-	let foo = produce((_: Draft<State>) => {})
+	type A = {readonly a: string}
+	type B = {readonly b: string}
+	type State = A | B
+	type Recipe = (state: State) => State
+
+	let foo = produce((draft: State) => {
+		assert(draft, _ as State)
+		if (Math.random() > 0.5) return {a: "test"}
+		else return {b: "boe"}
+	})
+	const x = foo({a: ""})
+	const y = foo({b: ""})
 	assert(foo, _ as Recipe)
 })
 
 it("can infer state type from recipe function with arguments", () => {
 	type State = {readonly a: string} | {readonly b: string}
-	type Recipe = <S extends State>(state: S, x: number) => S
+	type Recipe = (state: State, x: number) => State
 
-	let foo = produce((draft: Draft<State>, x: number) => {})
+	let foo = produce<State, [number]>((draft, x) => {
+		assert(draft, _ as Draft<State>)
+		assert(x, _ as number)
+	})
 	assert(foo, _ as Recipe)
 })
 
 it("can infer state type from recipe function with arguments and initial state", () => {
 	type State = {readonly a: string} | {readonly b: string}
-	type Recipe = <S extends State>(state: S | undefined, x: number) => S
+	type Recipe = (state: State | undefined, x: number) => State
 
 	let foo = produce((draft: Draft<State>, x: number) => {}, _ as State)
 	assert(foo, _ as Recipe)
@@ -155,7 +166,7 @@ describe("curried producer", () => {
 
 		// No initial state:
 		{
-			type Recipe = <S extends State>(state: S, a: number, b: number) => S
+			type Recipe = (state: State, a: number, b: number) => State
 			let foo = produce((s: State, a: number, b: number) => {})
 			assert(foo, _ as Recipe)
 			foo(_ as State, 1, 2)
@@ -163,7 +174,7 @@ describe("curried producer", () => {
 
 		// Using argument parameters:
 		{
-			type Recipe = <S extends State>(state: S, ...rest: number[]) => S
+			type Recipe = (state: Immutable<State>, ...rest: number[]) => Draft<State>
 			let woo = produce((state: Draft<State>, ...args: number[]) => {})
 			assert(woo, _ as Recipe)
 			woo(_ as State, 1, 2)
@@ -171,10 +182,7 @@ describe("curried producer", () => {
 
 		// With initial state:
 		{
-			type Recipe = <S extends State>(
-				state?: S | undefined,
-				...rest: number[]
-			) => S
+			type Recipe = (state?: State | undefined, ...rest: number[]) => State
 			let bar = produce((state: Draft<State>, ...args: number[]) => {},
 			_ as State)
 			assert(bar, _ as Recipe)
@@ -185,11 +193,11 @@ describe("curried producer", () => {
 
 		// When args is a tuple:
 		{
-			type Recipe = <S extends State>(
-				state: S | undefined,
+			type Recipe = (
+				state: State | undefined,
 				first: string,
 				...rest: number[]
-			) => S
+			) => State
 			let tup = produce(
 				(state: Draft<State>, ...args: [string, ...number[]]) => {},
 				_ as State
@@ -204,7 +212,7 @@ describe("curried producer", () => {
 		// No initial state:
 		{
 			let foo = produce((state: string[]) => {})
-			assert(foo, _ as <S extends readonly string[]>(state: S) => S)
+			assert(foo, _ as (state: readonly string[]) => string[])
 			foo([] as ReadonlyArray<string>)
 		}
 
@@ -213,7 +221,7 @@ describe("curried producer", () => {
 			let bar = produce(() => {}, [] as ReadonlyArray<string>)
 			assert(
 				bar,
-				_ as <S extends readonly string[]>(state?: S | undefined) => S
+				_ as (state?: readonly string[] | undefined) => readonly string[]
 			)
 			bar([] as ReadonlyArray<string>)
 			bar(undefined)
@@ -223,17 +231,17 @@ describe("curried producer", () => {
 })
 
 it("works with return type of: number", () => {
-	let base = _ as {a: number}
-	let result = produce(base, () => 1)
-	assert(result, _ as number)
-})
-
-it("works with return type of: number | undefined", () => {
 	let base = {a: 0} as {a: number}
-	let result = produce(base, draft => {
-		return draft.a < 0 ? 0 : undefined
-	})
-	assert(result, _ as {a: number} | number)
+	{
+		if (Math.random() === 100) {
+			// @ts-expect-error, this return accidentally a number, this is probably a dev error!
+			let result = produce(base, draft => draft.a++)
+		}
+	}
+	{
+		let result = produce(base, draft => void draft.a++)
+		assert(result, _ as {a: number})
+	}
 })
 
 it("can return an object type that is identical to the base type", () => {
@@ -241,16 +249,15 @@ it("can return an object type that is identical to the base type", () => {
 	let result = produce(base, draft => {
 		return draft.a < 0 ? {a: 0} : undefined
 	})
-	// TODO: Can we resolve the weird union of identical object types?
-	assert(result, _ as {a: number} | {a: number})
+	assert(result, _ as {a: number})
 })
 
-it("can return an object type that is _not_ assignable to the base type", () => {
+it("can NOT return an object type that is _not_ assignable to the base type", () => {
 	let base = {a: 0} as {a: number}
+	// @ts-expect-error
 	let result = produce(base, draft => {
 		return draft.a < 0 ? {a: true} : undefined
 	})
-	assert(result, _ as {a: number} | {a: boolean})
 })
 
 it("does not enforce immutability at the type level", () => {
@@ -261,25 +268,25 @@ it("does not enforce immutability at the type level", () => {
 })
 
 it("can produce an undefined value", () => {
-	let base = {a: 0} as {readonly a: number}
+	type State = {readonly a: number} | undefined
+	const base = {a: 0} as State
 
 	// Return only nothing.
 	let result = produce(base, _ => nothing)
-	assert(result, undefined)
+	assert(result, _ as State)
 
 	// Return maybe nothing.
 	let result2 = produce(base, draft => {
-		if (draft.a > 0) return nothing
+		if (draft?.a ?? 0 > 0) return nothing
 	})
-	assert(result2, _ as typeof base | undefined)
+	assert(result2, _ as State)
 })
 
 it("can return the draft itself", () => {
 	let base = _ as {readonly a: number}
 	let result = produce(base, draft => draft)
 
-	// Currently, the `readonly` modifier is lost.
-	assert(result, _ as {a: number})
+	assert(result, _ as {readonly a: number})
 })
 
 it("can return a promise", () => {
@@ -288,15 +295,15 @@ it("can return a promise", () => {
 
 	// Return a promise only.
 	let res1 = produce(base, draft => {
-		return Promise.resolve(draft.a > 0 ? null : undefined)
+		return Promise.resolve(draft)
 	})
-	assert(res1, _ as Promise<Base | null>)
+	assert(res1, _ as Promise<Base>)
 
 	// Return a promise or undefined.
 	let res2 = produce(base, draft => {
-		if (draft.a > 0) return Promise.resolve()
+		return Promise.resolve()
 	})
-	assert(res2, _ as Base | Promise<Base>)
+	assert(res2, _ as Promise<Base>)
 })
 
 it("works with `void` hack", () => {
@@ -331,13 +338,14 @@ it("can work with non-readonly base types", () => {
 	}
 	type State = typeof state
 
-	const newState: State = produce(state, draft => {
+	const newState = produce(state, draft => {
 		draft.price += 5
 		draft.todos.push({
 			title: "hi",
 			done: true
 		})
 	})
+	assert(newState, _ as State)
 
 	const reducer = (draft: State) => {
 		draft.price += 5
@@ -352,9 +360,9 @@ it("can work with non-readonly base types", () => {
 	assert(newState4, _ as State)
 	// no argument case, in that case, immutable version recipe first arg will be inferred
 	const newState5 = produce(reducer, state)()
-	assert(newState5, _ as Immutable<State>)
-	// we can force the return type of the reducer by passing the generic argument
-	const newState3 = produce(reducer, state)<State>()
+	assert(newState5, _ as State)
+	// we can force the return type of the reducer by casting the initial state
+	const newState3 = produce(reducer, state as State)()
 	assert(newState3, _ as State)
 })
 
@@ -377,13 +385,15 @@ it("can work with readonly base types", () => {
 		]
 	}
 
-	const newState: State = produce(state, draft => {
+	const newState = produce(state, draft => {
 		draft.price + 5
 		draft.todos.push({
 			title: "hi",
 			done: true
 		})
 	})
+	assert(newState, _ as State)
+	assert(newState, _ as Immutable<State>) // cause that is the same!
 
 	const reducer = (draft: Draft<State>) => {
 		draft.price += 5
@@ -400,9 +410,9 @@ it("can work with readonly base types", () => {
 	assert(newState4, _ as State)
 	// no argument case, in that case, immutable version recipe first arg will be inferred
 	const newState5 = produce(reducer, state)()
-	assert(newState5, _ as Immutable<State>)
-	// we can force the return type of the reducer by passing the generic argument
-	const newState3 = produce(reducer, state)<State>()
+	assert(newState5, _ as State)
+	// we can force the return type of the reducer by casting initial argument
+	const newState3 = produce(reducer, state as State)()
 	assert(newState3, _ as State)
 })
 
@@ -438,18 +448,18 @@ it("works with Map and Set", () => {
 
 it("works with readonly Map and Set", () => {
 	type S = {readonly x: number}
-	const m = new Map<string, S>([["a", {x: 1}]])
-	const s = new Set<S>([{x: 2}])
+	const m: ReadonlyMap<string, S> = new Map([["a", {x: 1}]])
+	const s: ReadonlySet<S> = new Set([{x: 2}])
 
 	const res1 = produce(m, (draft: Draft<Map<string, S>>) => {
 		assert(draft, _ as Map<string, {x: number}>)
 	})
-	assert(res1, _ as Map<string, {readonly x: number}>)
+	assert(res1, _ as ReadonlyMap<string, {readonly x: number}>)
 
 	const res2 = produce(s, (draft: Draft<Set<S>>) => {
 		assert(draft, _ as Set<{x: number}>)
 	})
-	assert(res2, _ as Set<{readonly x: number}>)
+	assert(res2, _ as ReadonlySet<{readonly x: number}>)
 })
 
 it("works with ReadonlyMap and ReadonlySet", () => {
@@ -494,3 +504,261 @@ it("#749 types Immer", () => {
 	// @ts-expect-error
 	expect(z.z).toBeUndefined()
 })
+
+it("infers draft, #720", () => {
+	function nextNumberCalculator(fn: (base: number) => number) {
+		// noop
+	}
+
+	const res2 = nextNumberCalculator(
+		produce(draft => {
+			// @ts-expect-error
+			let x: string = draft
+			return draft + 1
+		})
+	)
+
+	const res = nextNumberCalculator(
+		produce(draft => {
+			// @ts-expect-error
+			let x: string = draft
+			// return draft + 1;
+			return undefined
+		})
+	)
+})
+
+it("infers draft, #720 - 2", () => {
+	function useState<S>(
+		initialState: S | (() => S)
+	): [S, Dispatch<SetStateAction<S>>] {
+		return [initialState, function() {}] as any
+	}
+	type Dispatch<A> = (value: A) => void
+	type SetStateAction<S> = S | ((prevState: S) => S)
+
+	const [n, setN] = useState({x: 3})
+
+	setN(
+		produce(draft => {
+			// @ts-expect-error
+			draft.y = 4
+			draft.x = 5
+			return draft
+		})
+	)
+
+	setN(
+		produce(draft => {
+			// @ts-expect-error
+			draft.y = 4
+			draft.x = 5
+			// return draft + 1;
+			return undefined
+		})
+	)
+
+	setN(
+		produce(draft => {
+			return {y: 3} as const
+		})
+	)
+})
+
+it("infers draft, #720 - 3", () => {
+	function useState<S>(
+		initialState: S | (() => S)
+	): [S, Dispatch<SetStateAction<S>>] {
+		return [initialState, function() {}] as any
+	}
+	type Dispatch<A> = (value: A) => void
+	type SetStateAction<S> = S | ((prevState: S) => S)
+
+	const [n, setN] = useState({x: 3} as {readonly x: number})
+
+	setN(
+		produce(draft => {
+			// @ts-expect-error
+			draft.y = 4
+			draft.x = 5
+			return draft
+		})
+	)
+
+	setN(
+		produce(draft => {
+			// @ts-expect-error
+			draft.y = 4
+			draft.x = 5
+			// return draft + 1;
+			return undefined
+		})
+	)
+
+	setN(
+		produce(draft => {
+			return {y: 3} as const
+		})
+	)
+})
+
+it("infers curried", () => {
+	type Todo = {title: string}
+	{
+		const fn = produce((draft: Todo) => {
+			let x: string = draft.title
+		})
+
+		fn({title: "test"})
+		// @ts-expect-error
+		fn(3)
+	}
+	{
+		const fn = produce((draft: Todo) => {
+			let x: string = draft.title
+			return draft
+		})
+
+		fn({title: "test"})
+		// @ts-expect-error
+		fn(3)
+	}
+})
+
+it("infers async curried", async () => {
+	type Todo = {title: string}
+	{
+		const fn = produce(async (draft: Todo) => {
+			let x: string = draft.title
+		})
+
+		const res = await fn({title: "test"})
+		// @ts-expect-error
+		fn(3)
+		assert(res, _ as Todo)
+	}
+	{
+		const fn = produce(async (draft: Todo) => {
+			let x: string = draft.title
+			return draft
+		})
+
+		const res = await fn({title: "test"})
+		// @ts-expect-error
+		fn(3)
+		assert(res, _ as Todo)
+	}
+})
+
+{
+	type State = {count: number}
+	type ROState = Immutable<State>
+	const base: any = {count: 0}
+	{
+		// basic
+		const res = produce(base as State, draft => {
+			draft.count++
+		})
+		assert(res, _ as State)
+	}
+	{
+		// basic
+		const res = produce<State>(base, draft => {
+			draft.count++
+		})
+		assert(res, _ as State)
+	}
+	{
+		// basic
+		const res = produce(base as ROState, draft => {
+			draft.count++
+		})
+		assert(res, _ as ROState)
+	}
+	{
+		// curried
+		const f = produce((state: State) => {
+			state.count++
+		})
+		assert(f, _ as (state: Immutable<State>) => State)
+	}
+	{
+		// curried
+		const f = produce((state: Draft<ROState>) => {
+			state.count++
+		})
+		assert(f, _ as (state: ROState) => State)
+	}
+	{
+		// curried
+		const f: (value: State) => State = produce(state => {
+			state.count++
+		})
+	}
+	{
+		// curried
+		const f: (value: ROState) => ROState = produce(state => {
+			state.count++
+		})
+	}
+	{
+		// curried initial
+		const f = produce(state => {
+			state.count++
+		}, _ as State)
+		assert(f, _ as (state?: State) => State)
+	}
+	{
+		// curried initial
+		const f = produce(state => {
+			state.count++
+		}, _ as ROState)
+		assert(f, _ as (state?: ROState) => ROState)
+	}
+	{
+		// curried
+		const f: (value: State) => State = produce(state => {
+			state.count++
+		}, base as ROState)
+	}
+	{
+		// curried
+		const f: (value: ROState) => ROState = produce(state => {
+			state.count++
+		}, base as ROState)
+	}
+	{
+		// nothing allowed
+		const res = produce(base as State | undefined, draft => {
+			return nothing
+		})
+		assert(res, _ as State | undefined)
+	}
+	{
+		// nothing not allowed
+		// @ts-expect-error
+		produce(base as State, draft => {
+			return nothing
+		})
+	}
+	{
+		const f = produce((draft: State) => {})
+		const n = f(base as State)
+		assert(n, _ as State)
+	}
+	{
+		const f = produce((draft: Draft<ROState>) => {
+			draft.count++
+		})
+		const n = f(base as ROState)
+		assert(n, _ as State)
+	}
+	{
+		// explictly use generic
+		const f = produce<ROState>(draft => {
+			draft.count++
+		})
+		const n = f(base as ROState)
+		assert(n, _ as ROState) // yay!
+	}
+}
diff --git a/__tests__/redux.ts b/__tests__/redux.ts
index 6de78fa..7e598f5 100644
--- a/__tests__/redux.ts
+++ b/__tests__/redux.ts
@@ -1,101 +1,234 @@
 import {assert, _} from "spec.ts"
-import produce, {
-	produce as produce2,
-	applyPatches,
-	Patch,
-	nothing,
-	Draft,
-	Immutable,
-	enableES5
-} from "../src/immer"
+import produce, {Draft, enableES5, Immutable} from "../src/immer"
 import * as redux from "redux"
 
 enableES5()
 
-interface State {
-	counter: number
-}
+// Mutable Redux
+{
+	interface State {
+		counter: number
+	}
 
-interface Action {
-	type: string
-	payload: number
-}
+	interface Action {
+		type: string
+		payload: number
+	}
+
+	const initialState: State = {
+		counter: 0
+	}
+
+	/// =============== Actions
+
+	const reduceCounterProducer = (state: State = initialState, action: Action) =>
+		produce(state, draftState => {
+			switch (action.type) {
+				case "ADD_TO_COUNTER":
+					draftState.counter += action.payload
+					break
+				case "SUB_FROM_COUNTER":
+					draftState.counter -= action.payload
+					break
+			}
+		})
+
+	const reduceCounterCurriedProducer = produce(
+		(draftState: State, action: Action) => {
+			switch (action.type) {
+				case "ADD_TO_COUNTER":
+					draftState.counter += action.payload
+					break
+				case "SUB_FROM_COUNTER":
+					draftState.counter -= action.payload
+					break
+			}
+		},
+		initialState
+	)
+
+	/// =============== Reducers
+
+	const reduce = redux.combineReducers({
+		counterReducer: reduceCounterProducer
+	})
 
-const initialState: State = {
-	counter: 0
+	const curredReduce = redux.combineReducers({
+		counterReducer: reduceCounterCurriedProducer
+	})
+
+	// reducing the current state to get the next state!
+	// console.log(reduce(initialState, addToCounter(12));
+
+	// ================ store
+
+	const store = redux.createStore(reduce)
+	const curriedStore = redux.createStore(curredReduce)
+
+	it("#470 works with Redux combine reducers", () => {
+		assert(
+			store.getState().counterReducer,
+			_ as {
+				counter: number
+			}
+		)
+		assert(
+			curriedStore.getState().counterReducer,
+			_ as {
+				counter: number
+			}
+		)
+	})
 }
 
-/// =============== Actions
+// Readonly Redux
+{
+	{
+		interface State {
+			readonly counter: number
+		}
+
+		interface Action {
+			readonly type: string
+			readonly payload: number
+		}
 
-function addToCounter(addNumber: number) {
-	return {
-		type: "ADD_TO_COUNTER",
-		payload: addNumber
+		const initialState: State = {
+			counter: 0
+		}
+
+		/// =============== Actions
+
+		const reduceCounterProducer = (
+			state: State = initialState,
+			action: Action
+		) =>
+			produce(state, draftState => {
+				switch (action.type) {
+					case "ADD_TO_COUNTER":
+						draftState.counter += action.payload
+						break
+					case "SUB_FROM_COUNTER":
+						draftState.counter -= action.payload
+						break
+				}
+			})
+
+		const reduceCounterCurriedProducer = produce(
+			(draftState: Draft<State>, action: Action) => {
+				switch (action.type) {
+					case "ADD_TO_COUNTER":
+						draftState.counter += action.payload
+						break
+					case "SUB_FROM_COUNTER":
+						draftState.counter -= action.payload
+						break
+				}
+			},
+			initialState
+		)
+
+		/// =============== Reducers
+
+		const reduce = redux.combineReducers({
+			counterReducer: reduceCounterProducer
+		})
+
+		const curredReduce = redux.combineReducers({
+			counterReducer: reduceCounterCurriedProducer
+		})
+
+		// reducing the current state to get the next state!
+		// console.log(reduce(initialState, addToCounter(12));
+
+		// ================ store
+
+		const store = redux.createStore(reduce)
+		const curriedStore = redux.createStore(curredReduce)
+
+		it("#470 works with Redux combine readonly reducers", () => {
+			assert(
+				store.getState().counterReducer,
+				_ as {
+					readonly counter: number
+				}
+			)
+			assert(
+				curriedStore.getState().counterReducer,
+				_ as {
+					readonly counter: number
+				}
+			)
+		})
 	}
 }
 
-function subFromCounter(subNumber: number) {
-	return {
-		type: "SUB_FROM_COUNTER",
-		payload: subNumber
+it("works with inferred curried reducer", () => {
+	type State = {
+		count: number
 	}
-}
 
-const reduceCounterProducer = (state: State = initialState, action: Action) =>
-	produce(state, draftState => {
-		switch (action.type) {
-			case "ADD_TO_COUNTER":
-				draftState.counter += action.payload
-				break
-			case "SUB_FROM_COUNTER":
-				draftState.counter -= action.payload
-				break
-		}
-	})
+	type Action = {
+		type: "inc"
+		count: number
+	}
 
-const reduceCounterCurriedProducer = produce(
-	(draftState: Draft<State>, action: Action) => {
-		switch (action.type) {
-			case "ADD_TO_COUNTER":
-				draftState.counter += action.payload
-				break
-			case "SUB_FROM_COUNTER":
-				draftState.counter -= action.payload
-				break
-		}
-	},
-	initialState
-)
+	const defaultState = {
+		count: 3
+	}
 
-/// =============== Reducers
+	const store = redux.createStore(
+		produce((state: State, action: Action) => {
+			if (action.type === "inc") state.count += action.count
+			// @ts-expect-error
+			state.count2
+		}, defaultState)
+	)
 
-const reduce = redux.combineReducers({
-	counterReducer: reduceCounterProducer
-})
+	assert(store.getState(), _ as State)
+	store.dispatch({
+		type: "inc",
+		count: 2
+	})
 
-const curredReduce = redux.combineReducers({
-	counterReducer: reduceCounterCurriedProducer
+	store.dispatch({
+		// @ts-expect-error
+		type: "inc2",
+		count: 2
+	})
 })
 
-// reducing the current state to get the next state!
-// console.log(reduce(initialState, addToCounter(12));
+it("works with inferred curried reducer - readonly", () => {
+	type State = {
+		readonly count: number
+	}
 
-// ================ store
+	type Action = {
+		readonly type: "inc"
+		readonly count: number
+	}
 
-const store = redux.createStore(reduce)
-const curriedStore = redux.createStore(curredReduce)
+	const defaultState: State = {
+		count: 3
+	}
 
-it("#470 works with Redux combine reducers", () => {
-	assert(
-		store.getState().counterReducer,
-		_ as {
-			counter: number
-		}
-	)
-	assert(
-		curriedStore.getState().counterReducer,
-		_ as {
-			readonly counter: number
-		}
+	const store = redux.createStore(
+		produce((state: Draft<State>, action: Action) => {
+			if (action.type === "inc") state.count += action.count
+			// @ts-expect-error
+			state.count2
+		}, defaultState)
 	)
+
+	assert(store.getState(), _ as State)
+	store.dispatch({
+		type: "inc",
+		count: 2
+	})
+
+	store.dispatch({
+		// @ts-expect-error
+		type: "inc2",
+		count: 2
+	})
 })