Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ts 3.4 inference improvements #348

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
3 changes: 1 addition & 2 deletions __tests__/draft.ts
@@ -1,4 +1,4 @@
import {Draft, DraftArray} from "../dist/immer.js"
import {Draft} from "../dist/immer.js"

// For checking if a type is assignable to its draft type (and vice versa)
declare const toDraft: <T>(value: T) => Draft<T>
Expand All @@ -23,7 +23,6 @@ declare const _: any
// NOTE: As of 3.2.2, everything fails without "extends any"
const $ = <Value extends any>(val: ReadonlyArray<Value>) => {
val = _ as Draft<typeof val>
val = _ as DraftArray<typeof val>
let elem: Value = _ as Draft<Value>
}
}
Expand Down
56 changes: 39 additions & 17 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 @@ -70,27 +71,47 @@ it("can update readonly state via standard api", () => {
// NOTE: only when the function type is inferred
it("can infer state type from default state", () => {
type State = {readonly a:number} | boolean
type Recipe = (base?: State | undefined) => State
type Recipe = (base?: State | boolean) => State
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doh, that should just be 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((_) => {})
aleclarson marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -152,8 +173,9 @@ describe("curried producer", () => {

// With initial state:
let bar = produce((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 @@ -165,9 +187,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 @@ -262,8 +284,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
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -67,7 +67,7 @@
"regenerator-runtime": "^0.11.1",
"rimraf": "^2.6.2",
"seamless-immutable": "^7.1.3",
"typescript": "3.1.1",
"typescript": "3.4.3",
"yarn-or-npm": "^2.0.4"
},
"jest": {
Expand Down
105 changes: 40 additions & 65 deletions src/immer.d.ts
@@ -1,3 +1,7 @@
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 All @@ -12,28 +16,19 @@ type AtomicObject =
| Number
| String

type ArrayMethod = Exclude<keyof [], number>
type Indices<T> = Exclude<keyof T, ArrayMethod>

export type DraftArray<T extends ReadonlyArray<any>> = Array<
{[P in Indices<T>]: Draft<T[P]>}[Indices<T>]
>

export type DraftTuple<T extends ReadonlyArray<any>> = {
[P in keyof T]: P extends Indices<T> ? Draft<T[P]> : never
}
export type Draft<T> = T extends AtomicObject
? T
: T extends object
? { -readonly [K in keyof T]: Draft<T[K]> }
: T // mostly: unknown & any

export type Draft<T> = T extends never[]
? T
: T extends ReadonlyArray<any>
? T[number][] extends T
? DraftArray<T>
: DraftTuple<T>
: T extends AtomicObject
? T
: T extends object
? {-readonly [P in keyof T]: Draft<T[P]>}
: T
/** Convert a mutable type into a readonly type */
export type Immutable<T> =
T extends AtomicObject
? T
: T extends object
? { readonly [K in keyof T]: Immutable<T[K]> }
: T

export interface Patch {
op: "replace" | "remove" | "add"
Expand All @@ -52,26 +47,6 @@ export type Produced<Base, Return> = Return extends void
: Return extends Promise<infer Result>
? Promise<Result extends void ? Base : FromNothing<Result>>
: FromNothing<Return>

type ImmutableArray<T extends ReadonlyArray<any>> = {
[P in Extract<keyof T, number>]: ReadonlyArray<Immutable<T[number]>>
}[Extract<keyof T, number>]

type ImmutableTuple<T extends ReadonlyArray<any>> = {
readonly [P in keyof T]: Immutable<T[P]>
}

/** Convert a mutable type into a readonly type */
export type Immutable<T> = T extends object
? T extends AtomicObject
? T
: T extends ReadonlyArray<any>
? Array<T[number]> extends T
? ImmutableArray<T>
: ImmutableTuple<T>
: {readonly [P in keyof T]: Immutable<T[P]>}
: T

export interface IProduce {
/**
* The `produce` function takes a value and a "recipe function" (whose
Expand All @@ -92,32 +67,32 @@ 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>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating Base and D allows things like: produce<readonly string[]>([], (draft: string[]) => ...

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice, this ...rest: Rest always resolved into ...any[], as soon as Base is set (not inferred) by using produce<X>((draft, x: number) => {})

) => 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
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -6049,10 +6049,10 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"

typescript@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.1.tgz#3362ba9dd1e482ebb2355b02dfe8bcd19a2c7c96"
integrity sha512-Veu0w4dTc/9wlWNf2jeRInNodKlcdLgemvPsrNpfu5Pq39sgfFjvIIgTsvUHCoLBnMhPoUA+tFxsXjU6VexVRQ==
typescript@3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.3.tgz#0eb320e4ace9b10eadf5bc6103286b0f8b7c224f"
integrity sha512-FFgHdPt4T/duxx6Ndf7hwgMZZjZpB+U0nMNGVCYPq0rEzWKjEDobm4J6yb3CS7naZ0yURFqdw9Gwc7UOh/P9oQ==

uglify-js@^3.1.4:
version "3.5.3"
Expand Down