Reputation: 2046
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
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
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
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