Immer: Draft type clobbers nested object types in typescript 3.1.3

Created on 7 Nov 2018  路  12Comments  路  Source: immerjs/immer

import { Draft } from "immer";

interface State {
  x: { y: string };
}

type T0 = Draft<State> // $ExpectType State
  • Expected behavior: type T0 has type identical to state
  • Observed behavior: type T0 has type { x: any }

The issue stems from the mutual recursion between DraftObject and Draft.
It is reproducible with (typescript playground):

type DraftObject<T> = T extends object ? {-readonly [P in keyof T]: Draft<T[P]>} : T
type Draft<T> = T extends object ? DraftObject<T> : T
type T0 = Draft<{ x: { y: string } }>

where Draft is identical to Draft from immer, but with cases for arrays and tuples removed

bug has PR

All 12 comments

If the conditional types are removed from DraftObject it appears to work (typescript playground):

type DraftObject<T> = {-readonly [P in keyof T]: Draft<T[P]>}
type Draft<T> = T extends object ? DraftObject<T> : T
type T0 = Draft<{ x: { y: string } }>

Whoops! Didn't think to test interfaces. Bug confirmed! 馃憤

Looking into a fix...

If we extract the mapped type into its own type, then the issue doesn't occur (typescript playground):

type AtomicObject =
  | Function
  | Map<any, any>
  | WeakMap<any, any>
  | Set<any>
  | WeakSet<any>
  | Promise<any>
  | Date
  | RegExp
  | Boolean
  | Number
  | String

type IsFinite<T extends any[]> = T extends never[]
  ? true
  : T extends ReadonlyArray<infer U> ? (U[] extends T ? false : true) : true

// we extracted the mapped type into its own name
export type DraftObjectReal<T> = { -readonly [P in keyof T]: Draft<T[P]> }
export type DraftObject<T> = T extends object
  ? T extends AtomicObject ? T : DraftObjectReal<T>
  : T

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

export type DraftTuple<T extends any[]> = {
  [P in keyof T]: T[P] extends T[number] ? Draft<T[P]> : never
}

export type Draft<T> = T extends any[]
  ? IsFinite<T> extends true ? DraftTuple<T> : DraftArray<T[number]>
  : T extends ReadonlyArray<any>
    ? DraftArray<T[number]>
    : T extends object ? DraftObject<T> : T

type T0 = Draft<{ x: { y: string } }>

Its probably an upstream problem with typescript, but I'm not sure they'll fix it since it deals with type recursion and they don't really support that.

Extracting the mapped type out does seem to work, and it wouldn't break anything, so its probably a good fix

I need to figure out how to trigger a compiler error when specificity is _lost_ (ie: expected an object type, got any) for the tests that missed this bug.

Yeah, I know you can use dtslint to test types, but I'm not sure if it tests them exactly or just if they're assignable

This works! 馃帀

type Exact<A, B> = A extends B ? (B extends A ? 1 : 0) : 0
let exact = <A, B>(): Exact<A, B> => b

let val = {a: 1}
let test: 1 = exact<any, typeof val>()

Quick update: I'll try to find time to fix all the errors found by #244 sometime soon

:tada: This issue has been resolved in version 1.9.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Just trying out immer (which is fantastic, thanks!) and I seem to still be running into this with typescript 3.1.4 and immer 1.9.3.

In the following example:

type Params = {
    users?: {
      [userId: string]: {
        [parentId: string]: {id: string}
      }
    }
}

const params: Params = {};
const parent = {id: "id"};

return produce(params, draft => {
   if(!draft.users)draft.users = {};
   if(!draft.users[userId])draft.users[userId] = {};
   draft.users[userId][parentId] = parent;
})

The inferred type of draft is { users?: any } instead of the full Params type.

If I explicitly set (draft: Params)=> ... then I get the full nested type, but it would of course be nice to have the inference.

@danenania Confirmed on typescript@^3.1.6. Upgrading to ^3.2.2 fixes it.

If someone wants to contribute a patch, go right ahead. 馃憤

Ah great, will upgrade typescript. Thanks!

Was this page helpful?
0 / 5 - 0 ratings