Hayk Safaryan
Hayk Safaryan

Reputation: 2046

TypeScript Immer Argument of type is not assignable to parameter of type DraftArray

I am trying to use the immer https://github.com/mweststrate/immer for my reducers but get the following error from typescript

Argument of type 'ReadonlyArray<IBidding>' is not assignable to parameter of type '(this: DraftArray<IBidding>, draftState: DraftArray<IBidding>, ...extraArgs: any[]) => void | Rea...'.
  Type 'ReadonlyArray<IBidding>' provides no match for the signature '(this: DraftArray<IBidding>, draftState: DraftArray<IBidding>, ...extraArgs: any[]): void | ReadonlyArray<IBidding>'.

I have types.ts like this

export interface IBidding {
  readonly id: string
  readonly ownerId: string
  readonly name: string
  readonly description: string
  readonly startDate: Date
  readonly endDate: Date
  readonly suppliers: ReadonlyArray<ISupplier>
  readonly inquiryCreator: string
  readonly bidStep: number,
  readonly bids: ReadonlyArray<IBid>,
  readonly startingBid: number,
  readonly img: string
}

interface ISupplier {
  id: string
  name: string
}

interface IBid {
  ownerId: string
  value: number
  createdAt: Date
}

export type IBiddingsState = ReadonlyArray<IBidding>

export const enum BiddingsActionTypes {
  BID = '@@biddings/BID'
}

And here is my reducer.ts

import { Reducer } from 'redux'

import produce from 'immer'

import { IBiddingsState, BiddingsActionTypes } from './types'
import { biddingsReducerInitialState as initialState } from './fixtures'

/**
 * Reducer for biddings list
 */
const reducer: Reducer<IBiddingsState> = (state = initialState, action) => {
  return produce<IBiddingsState>(state, draft => {
    switch (action.type) {
      case BiddingsActionTypes.BID:
        const {
          biddingId,
          bid
        } = action.payload

        const biddingIndex = draft.findIndex(elem => elem.id === biddingId)
        draft[biddingIndex].bids.push(bid)
        return draft
      default: {
        return state
      }
    }
  })
}

export { reducer as biddingsReducer }

It seems I did everything as in the docs, but still getting the error. Why is that happening?

Upvotes: 3

Views: 9171

Answers (3)

wctiger
wctiger

Reputation: 1061

I had a similar issue today, I am on immer 7.0.9. Looked up in the documentation and they do mention a utility function castDraft as a workaround. https://immerjs.github.io/immer/docs/typescript#cast-utilities

Upvotes: 4

Joe
Joe

Reputation: 71

You weren't alone! Immer has been updated and should hopefully help you in this exact situation. Here's the change:

https://github.com/mweststrate/immer/commit/512256bbde4ea1e2b6a75399d6ad59925752ad6b

So long as you're using TypeScript 2.8.x or higher you should be good to go.

This change is live as of immer 1.7.4.

Upvotes: 1

Matt McCutchen
Matt McCutchen

Reputation: 30899

Unfortunately, when a call to an overloaded function like produce, doesn't match any of the overloads, TypeScript is pretty bad at guessing which overload you intended to give a meaningful report of which argument is wrong. If you add some type annotations to the recipe:

const reducer: Reducer<IBiddingsState> = (state = initialState, action) => {
  return produce<IBiddingsState>(state, (draft: Draft<IBiddingsState>): IBiddingsState => {
    switch (action.type) {
      case BiddingsActionTypes.BID:
        const {
          biddingId,
          bid
        } = action.payload

        const biddingIndex = draft.findIndex(elem => elem.id === biddingId)
        draft[biddingIndex].bids.push(bid)
        return draft
      default: {
        return state
      }
    }
  })
}

then you see that the problem is with the return draft and you get a little more information:

[ts]
Type 'DraftArray<IBidding>' is not assignable to type 'ReadonlyArray<IBidding>'.
  Types of property 'includes' are incompatible.
    Type '(searchElement: DraftObject<IBidding>, fromIndex?: number) => boolean' is not assignable to type '(searchElement: IBidding, fromIndex?: number) => boolean'.
      Types of parameters 'searchElement' and 'searchElement' are incompatible.
        Type 'IBidding' is not assignable to type 'DraftObject<IBidding>'.
          Types of property 'suppliers' are incompatible.
            Type 'ReadonlyArray<ISupplier>' is not assignable to type 'DraftArray<ISupplier>'.
              Property 'push' is missing in type 'ReadonlyArray<ISupplier>'.

Arrays are supposed to be covariant, and in support of that, the first parameter to includes is bivariant, but unfortunately TypeScript has guessed the wrong direction to report a failure. We expect DraftObject<IBidding> to be assignable to IBidding, not the other way around. If we test that directly:

import { Draft } from 'immer'
import { IBidding } from './types'
let x: Draft<IBidding>;
let y: IBidding = x;

then we finally see the root cause:

[ts]
Type 'DraftObject<IBidding>' is not assignable to type 'IBidding'.
  Types of property 'startDate' are incompatible.
    Type 'DraftObject<Date>' is not assignable to type 'Date'.
      Property '[Symbol.toPrimitive]' is missing in type 'DraftObject<Date>'.

And this is because DraftObject is defined as follows:

// Mapped type to remove readonly modifiers from state
// Based on https://github.com/Microsoft/TypeScript/blob/d4dc67aab233f5a8834dff16531baf99b16fea78/tests/cases/conformance/types/conditional/conditionalTypes1.ts#L120-L129
export type DraftObject<T> = {
  -readonly [P in keyof T]: Draft<T[P]>;
};

and the keyof doesn't include well-known symbols such as Symbol.toPrimitive (TypeScript issue).

As a workaround, you could fork the immer types and modify the definition of Draft as follows:

export type Draft<T> =
  T extends any[] ? DraftArray<T[number]> :
  T extends ReadonlyArray<any> ? DraftArray<T[number]> :
  T extends Date ? Date :  // <-- insert this line
  T extends object ? DraftObject<T> :
  T;

Or if you don't have many occurrences, just add type assertions to your code as necessary.

Upvotes: 6

Related Questions