From acf8a57ed9e1c8c41c759d104acac8d29559f2e2 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 10 Apr 2019 22:46:35 +0200 Subject: [PATCH] feat(ts): Improve type inference based on the first arg of the recipe function --- __tests__/curry.js | 1 + __tests__/produce.ts | 57 +++++++++++++++++++++++++++++------------ src/immer.d.ts | 61 +++++++++++++++++++++++++++----------------- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/__tests__/curry.js b/__tests__/curry.js index 897ff5e0..3ad7b905 100644 --- a/__tests__/curry.js +++ b/__tests__/curry.js @@ -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", () => { diff --git a/__tests__/produce.ts b/__tests__/produce.ts index 90bc5f5c..d85cf225 100644 --- a/__tests__/produce.ts +++ b/__tests__/produce.ts @@ -3,7 +3,8 @@ import produce, { applyPatches, Patch, nothing, - Draft + Draft, + Immutable } from "../dist/immer.js" // prettier-ignore @@ -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, arg: number) => {}, {} as any) + let foo = produce((draft: Draft) => {}) + 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, 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, 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(draft => { + const newState = produce((draft: Draft) => { draft.num++ draft.foo = "bar" draft.bar = "foo" @@ -106,7 +127,7 @@ it("can update readonly state via curried api", () => { }) it("can update use the non-default export", () => { - const newState = produce2(draft => { + const newState = produce2((draft: Draft) => { draft.num++ draft.foo = "bar" draft.bar = "foo" @@ -141,8 +162,8 @@ describe("curried producer", () => { type State = {readonly a: 1} // No initial state: - let foo = produce(() => {}) - 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 @@ -155,8 +176,12 @@ describe("curried producer", () => { (state: Draft, ...args: number[]) => {}, {} as State ) - exactType(bar, {} as (base?: State, ...args: number[]) => State) - bar({} as State | undefined, 1, 2) + exactType(bar, {} as ( + base?: undefined | Immutable, + ...args: number[] + ) => State) + bar({} as State, 1, 2) + bar({} as State) bar() // When args is a tuple: @@ -175,9 +200,9 @@ describe("curried producer", () => { it("can be passed a readonly array", () => { // No initial state: - let foo = produce>(() => {}) - exactType(foo, {} as (base: readonly any[]) => readonly any[]) - foo([] as ReadonlyArray) + let foo = produce((state: string[]) => {}) + exactType(foo, {} as (base: readonly string[]) => readonly string[]) + foo([] as ReadonlyArray) // With initial state: let bar = produce(() => {}, [] as ReadonlyArray) @@ -272,8 +297,8 @@ it("works with `void` hack", () => { }) it("works with generic parameters", () => { - let insert = (array: ReadonlyArray, index: number, elem: T) => { - // NOTE: As of 3.2.2, the explicit argument type is required. + let insert = (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) diff --git a/src/immer.d.ts b/src/immer.d.ts index 2621d21a..84b1e148 100644 --- a/src/immer.d.ts +++ b/src/immer.d.ts @@ -1,3 +1,10 @@ +type Tail = ((...t: T) => any) extends (( + _: any, + ...tail: infer TT +) => any) + ? TT + : [] + /** Object types that should never be mapped */ type AtomicObject = | Function @@ -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: Base extends Function ? never : Base, - recipe: (this: Draft, draft: Draft) => Return, + + /** Curried producer */ + < + Recipe extends (...args: any[]) => any, + Params extends any[] = Parameters, + T = Params[0] + >( + recipe: Recipe + ): ( + state: Immutable, + ...rest: Tail + ) => Produced, ReturnType> + + /** Curried producer with initial state */ + < + Recipe extends (...args: any[]) => any, + Params extends any[] = Parameters, + T = Params[0] + >( + recipe: Recipe, + initialState: T + ): ( + state?: Immutable, + ...rest: Tail + ) => Produced, ReturnType> + + /** Normal producer */ + , Return = void>( + base: Base, + recipe: (this: D, draft: D) => Return, listener?: PatchListener ): Produced - - /** Curried producer with a default value */ - ( - recipe: (this: Base, draft: Base, ...rest: Rest) => Return, - defaultBase: Immutable - ): Rest[number][] extends Rest | never[] - ? ( - // The `base` argument is optional when `Rest` is optional. - base?: Immutable, - ...rest: Rest - ) => Produced, Return> - : ( - // The `base` argument is required when `Rest` is required. - base: Immutable | undefined, - ...rest: Rest - ) => Produced, Return> - - /** Curried producer with no default value */ - ( - recipe: (this: Draft, draft: Draft, ...rest: Rest) => Return - ): (base: Immutable, ...rest: Rest) => Produced } export const produce: IProduce