Skip to content

Commit

Permalink
feat(ts): Improve type inference based on the first arg of the recipe…
Browse files Browse the repository at this point in the history
… function
  • Loading branch information
mweststrate authored and aleclarson committed Apr 17, 2019
1 parent 7e971b6 commit acf8a57
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 40 deletions.
1 change: 1 addition & 0 deletions __tests__/curry.js
Expand Up @@ -54,6 +54,7 @@ function runTests(name, useProxies) {

expect(reducer(undefined, 3)).toEqual({hello: "world", index: 3})
expect(reducer({}, 3)).toEqual({index: 3})
expect(reducer()).toEqual({hello: "world", index: undefined})
})

it("can has fun with change detection", () => {
Expand Down
57 changes: 41 additions & 16 deletions __tests__/produce.ts
Expand Up @@ -3,7 +3,8 @@ import produce, {
applyPatches,
Patch,
nothing,
Draft
Draft,
Immutable
} from "../dist/immer.js"

// prettier-ignore
Expand Down Expand Up @@ -72,25 +73,45 @@ it("can infer state type from default state", () => {
type State = {readonly a: number} | boolean
type Recipe = (base?: State | undefined) => State

let foo = produce(_ => {}, {} as State)
let foo = produce((x: any) => {}, {} as State)
exactType(foo, {} as Recipe)
})

it("can infer state type from recipe function", () => {
type State = {readonly a: string} | {readonly b: string}
type Recipe = (base: State | undefined, arg: number) => State
type Recipe = (base: State) => State

let foo = produce((draft: Draft<State>, arg: number) => {}, {} as any)
let foo = produce((draft: Draft<State>) => {})
exactType(foo, {} as Recipe)
})

it("can infer state type from recipe function with arguments", () => {
type State = {readonly a: string} | {readonly b: string}
type Recipe = (base: State, x: number) => State

let foo = produce((draft: Draft<State>, x: number) => {})
exactType(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 = (base: State | undefined, x: number) => State

let foo = produce((draft: Draft<State>, x: number) => {}, {} as State)
exactType(foo, {} as Recipe)
})

it("cannot infer state type when the function type and default state are missing", () => {
const res = produce(_ => {})
const res = produce((_: any) => {})
exactType(res, {} as (base: any) => any)

// slightly different type inference...
const res2 = produce(_ => {})
exactType(res2, {} as (base: any, ...rest: any[]) => any)
})

it("can update readonly state via curried api", () => {
const newState = produce<State>(draft => {
const newState = produce((draft: Draft<State>) => {
draft.num++
draft.foo = "bar"
draft.bar = "foo"
Expand All @@ -106,7 +127,7 @@ it("can update readonly state via curried api", () => {
})

it("can update use the non-default export", () => {
const newState = produce2<State>(draft => {
const newState = produce2((draft: Draft<State>) => {
draft.num++
draft.foo = "bar"
draft.bar = "foo"
Expand Down Expand Up @@ -141,8 +162,8 @@ describe("curried producer", () => {
type State = {readonly a: 1}

// No initial state:
let foo = produce<State, number[]>(() => {})
exactType(foo, {} as (base: State, ...args: number[]) => State)
let foo = produce((s: State, a: number, b: number) => {})
exactType(foo, {} as (base: State, x: number, y: number) => State)
foo({} as State, 1, 2)

// TODO: Using argument parameters
Expand All @@ -155,8 +176,12 @@ describe("curried producer", () => {
(state: Draft<State>, ...args: number[]) => {},
{} as State
)
exactType(bar, {} as (base?: State, ...args: number[]) => State)
bar({} as State | undefined, 1, 2)
exactType(bar, {} as (
base?: undefined | Immutable<State>,
...args: number[]
) => State)
bar({} as State, 1, 2)
bar({} as State)
bar()

// When args is a tuple:
Expand All @@ -175,9 +200,9 @@ describe("curried producer", () => {

it("can be passed a readonly array", () => {
// No initial state:
let foo = produce<ReadonlyArray<any>>(() => {})
exactType(foo, {} as (base: readonly any[]) => readonly any[])
foo([] as ReadonlyArray<any>)
let foo = produce((state: string[]) => {})
exactType(foo, {} as (base: readonly string[]) => readonly string[])
foo([] as ReadonlyArray<string>)

// With initial state:
let bar = produce(() => {}, [] as ReadonlyArray<any>)
Expand Down Expand Up @@ -272,8 +297,8 @@ it("works with `void` hack", () => {
})

it("works with generic parameters", () => {
let insert = <T>(array: ReadonlyArray<T>, index: number, elem: T) => {
// NOTE: As of 3.2.2, the explicit argument type is required.
let insert = <T>(array: readonly T[], index: number, elem: T) => {
// Need explicit cast on draft as T[] is wider than readonly T[]
return produce(array, (draft: T[]) => {
draft.push(elem)
draft.splice(index, 0, elem)
Expand Down
61 changes: 37 additions & 24 deletions src/immer.d.ts
@@ -1,3 +1,10 @@
type Tail<T extends any[]> = ((...t: T) => any) extends ((
_: any,
...tail: infer TT
) => any)
? TT
: []

/** Object types that should never be mapped */
type AtomicObject =
| Function
Expand Down Expand Up @@ -62,32 +69,38 @@ export interface IProduce {
* @param {Function} patchListener - optional function that will be called with all the patches produced here
* @returns {any} a new state, or the initial state if nothing was modified
*/
<Base = any, Return = void>(
base: Base extends Function ? never : Base,
recipe: (this: Draft<Base>, draft: Draft<Base>) => Return,

/** Curried producer */
<
Recipe extends (...args: any[]) => any,
Params extends any[] = Parameters<Recipe>,
T = Params[0]
>(
recipe: Recipe
): (
state: Immutable<T>,
...rest: Tail<Params>
) => Produced<Immutable<T>, ReturnType<Recipe>>

/** Curried producer with initial state */
<
Recipe extends (...args: any[]) => any,
Params extends any[] = Parameters<Recipe>,
T = Params[0]
>(
recipe: Recipe,
initialState: T
): (
state?: Immutable<T>,
...rest: Tail<Params>
) => Produced<Immutable<T>, ReturnType<Recipe>>

/** Normal producer */
<Base, D = Draft<Base>, Return = void>(
base: Base,
recipe: (this: D, draft: D) => Return,
listener?: PatchListener
): Produced<Base, Return>

/** Curried producer with a default value */
<Base = any, Rest extends any[] = [], Return = void>(
recipe: (this: Base, draft: Base, ...rest: Rest) => Return,
defaultBase: Immutable<Base>
): Rest[number][] extends Rest | never[]
? (
// The `base` argument is optional when `Rest` is optional.
base?: Immutable<Base>,
...rest: Rest
) => Produced<Immutable<Base>, Return>
: (
// The `base` argument is required when `Rest` is required.
base: Immutable<Base> | undefined,
...rest: Rest
) => Produced<Immutable<Base>, Return>

/** Curried producer with no default value */
<Base = any, Rest extends any[] = [], Return = void>(
recipe: (this: Draft<Base>, draft: Draft<Base>, ...rest: Rest) => Return
): (base: Immutable<Base>, ...rest: Rest) => Produced<Base, Return>
}

export const produce: IProduce
Expand Down

0 comments on commit acf8a57

Please sign in to comment.