redux - TypeScript Immer Argument av typen är inte tilldelningsbar till parametern av typen DraftArray


Jag försöker använda immer https://github.com/mweststrate/immer för mina reducers men får följande fel från 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>'.

Jag har types.ts som detta

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'
}

Och här är min 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 }

Det verkar som om jag gjorde allt enligt dokumentationen, men får ändå felet. Varför händer det?

Svar #1

Tyvärr, när ett anrop till en överlagrad funktion som produce inte matchar någon av överlagringarna, är TypeScript ganska dålig på att gissa vilken överlagring du avsåg för att ge en meningsfull rapport om vilken argument som är fel. Om du lägger till några typanteckningar i receptet:

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
      }
    }
  })
}

då ser du att problemet är med return draft och du får lite mer 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>'.

Arrayer förväntas vara kovarianta, och som stöd för det är den första parametern till includes bivariat, men tyvärr har TypeScript gissat fel riktning för att rapportera ett fel. Vi förväntar oss att DraftObject<IBidding> ska kunna tilldelas IBidding, inte tvärtom. Om vi testar det direkt:

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

då ser vi äntligen den grundläggande orsaken:

[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>'.

Och detta beror på att DraftObject definieras enligt följande:

// 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]>;
};

och keyof inkluderar inte välkända symboler såsom Symbol.toPrimitive (TypeScript-problem).

Som en tillfällig lösning kan du förgrena immer-typerna och ändra definitionen av Draft på följande sätt:

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;

Eller om du inte har många förekomster, lägg bara till typtolkningar i din kod efter behov.

Svar #2

Jag hade ett liknande problem idag, jag använder immer 7.0.9. Jag kollade i dokumentationen och de nämner en hjälpfunktion castDraft som en lösning. https://immerjs.github.io/immer/docs/typescript#cast-utilities

Relaterade artiklar