Skip to content

Commit dd25143

Browse files
committed
fix(ts): ensure produce returns an immutable type
1 parent 4bf4154 commit dd25143

File tree

3 files changed

+91
-8
lines changed

3 files changed

+91
-8
lines changed

__tests__/immutable.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {Immutable} from "../dist/immer.js"
2+
3+
// prettier-ignore
4+
type Exact<A, B> = (<T>() => T extends A ? 1 : 0) extends (<T>() => T extends B ? 1 : 0)
5+
? (A extends B ? (B extends A ? unknown : never) : never)
6+
: never
7+
8+
/** Fails when `actual` and `expected` have different types. */
9+
declare const exactType: <Actual, Expected>(
10+
actual: Actual & Exact<Actual, Expected>,
11+
expected: Expected & Exact<Actual, Expected>
12+
) => Expected
13+
14+
// array in tuple
15+
{
16+
let val = {} as Immutable<[string[], 1]>
17+
exactType(val, {} as [ReadonlyArray<string>, 1])
18+
}
19+
20+
// tuple in array
21+
{
22+
let val = {} as Immutable<[string, 1][]>
23+
exactType(val, {} as ReadonlyArray<[string, 1]>)
24+
}
25+
26+
// tuple in tuple
27+
{
28+
let val = {} as Immutable<[[string, 1], 1]>
29+
exactType(val, {} as [[string, 1], 1])
30+
}
31+
32+
// array in array
33+
{
34+
let val = {} as Immutable<string[][]>
35+
exactType(val, {} as ReadonlyArray<ReadonlyArray<string>>)
36+
}
37+
38+
// tuple in object
39+
{
40+
let val = {} as Immutable<{a: [string, 1]}>
41+
exactType(val, {} as {readonly a: [string, 1]})
42+
}
43+
44+
// object in tuple
45+
{
46+
let val = {} as Immutable<[{a: string}, 1]>
47+
exactType(val, {} as [{readonly a: string}, 1])
48+
}
49+
50+
// array in object
51+
{
52+
let val = {} as Immutable<{a: string[]}>
53+
exactType(val, {} as {readonly a: ReadonlyArray<string>})
54+
}
55+
56+
// object in array
57+
{
58+
let val = {} as Immutable<Array<{a: string}>>
59+
exactType(val, {} as ReadonlyArray<{readonly a: string}>)
60+
}
61+
62+
// object in object
63+
{
64+
let val = {} as Immutable<{a: {b: string}}>
65+
exactType(val, {} as {readonly a: {readonly b: string}})
66+
}

__tests__/produce.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import produce, {
33
applyPatches,
44
Patch,
55
nothing,
6-
Draft
6+
Draft,
7+
Immutable
78
} from "../dist/immer.js"
89

910
// prettier-ignore
@@ -72,7 +73,7 @@ it("can update readonly state via standard api", () => {
7273
it("can infer state type from default state", () => {
7374
type Producer = <T>(
7475
base: (Draft<T> extends number ? T : number) | undefined
75-
) => number
76+
) => Immutable<T>
7677
let foo = produce(_ => {}, 1)
7778
exactType(foo, {} as Producer)
7879
exactType(foo(2), 0 as number)
@@ -83,11 +84,12 @@ it("can infer state type from recipe function", () => {
8384
type Producer = <T>(
8485
base: (Draft<T> extends Base ? T : Base) | undefined,
8586
_2: number
86-
) => Base
87+
) => Immutable<T>
8788

8889
let foo = produce((_: string | number, _2: number) => {}, 1)
8990
exactType(foo, {} as Producer)
90-
exactType(foo("", 0), {} as string | number)
91+
exactType(foo("", 0), {} as string)
92+
exactType(foo(0, 0), {} as number)
9193
})
9294

9395
it("cannot infer state type when the function type and default state are missing", () => {
@@ -146,7 +148,7 @@ it("can provide rest parameters to a curried producer", () => {
146148
base: Draft<T> extends {} ? T : object,
147149
_2: number,
148150
_3: number
149-
) => object
151+
) => Immutable<T>
150152
let foo = produce((_1: object, _2: number, _3: number) => {})
151153
exactType(foo, {} as Foo)
152154
foo({}, 1, 2)
@@ -156,7 +158,7 @@ it("can provide rest parameters to a curried producer", () => {
156158
base: (Draft<T> extends {} ? T : object) | undefined,
157159
_2: number,
158160
_3: number
159-
) => object
161+
) => Immutable<T>
160162
let bar = produce((_1: object, _2: number, _3: number) => {}, {})
161163
exactType(bar, {} as Bar)
162164
bar(undefined, 1, 2)

src/immer.d.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,25 @@ type FromNothing<T> = Nothing extends T ? Exclude<T, Nothing> | undefined : T
5858
/** The inferred return type of `produce` */
5959
type Produced<Base, Return> = 1 extends HasVoidLike<Return>
6060
? 1 extends IsVoidLike<Return>
61-
? Base
62-
: Base | FromNothing<Exclude<Return, void>>
61+
? Immutable<Base>
62+
: Immutable<Base> | FromNothing<Exclude<Return, void>>
6363
: FromNothing<Return>
6464

65+
type ImmutableTuple<T extends ReadonlyArray<any>> = {
66+
readonly [P in keyof T]: Immutable<T[P]>
67+
}
68+
69+
/** Convert a mutable type into a readonly type */
70+
export type Immutable<T> = T extends object
71+
? T extends AtomicObject
72+
? T
73+
: T extends ReadonlyArray<any>
74+
? Array<T[number]> extends T
75+
? {[P in keyof T]: ReadonlyArray<Immutable<T[number]>>}[keyof T]
76+
: ImmutableTuple<T>
77+
: {readonly [P in keyof T]: Immutable<T[P]>}
78+
: T
79+
6580
export interface IProduce {
6681
/**
6782
* The `produce` function takes a value and a "recipe function" (whose

0 commit comments

Comments
 (0)