Typescript immer аргумент типа не может быть назначен параметру типа draftarray

Я пытаюсь использовать immerhttps://github.com/mweststrate/immer для своих редукторов, но получаю следующую ошибку из машинописного текста

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

У меня types.ts вот такой

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

А вот и мой 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 }

Кажется, я сделал все как в документации, но все равно получаю ошибку. Почему так происходит?

1
0
3 612
3

Ответы 3

К сожалению, когда вызов перегруженной функции, такой как produce, не соответствует ни одной из перегрузок, TypeScript довольно плохо угадывает, какую перегрузку вы намеревались дать содержательный отчет о том, какой аргумент неверен. Если вы добавите к рецепту аннотации типа:

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

то вы видите, что проблема в return draft, и получаете немного больше информации:

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

Массивы должны быть ковариантными, и в подтверждение этого первый параметр includes является двухвариантным, но, к сожалению, TypeScript угадал неверное направление для сообщения о сбое. Мы ожидаем, что DraftObject<IBidding> можно будет присвоить IBidding, а не наоборот. Если мы проверим это напрямую:

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

тогда мы наконец видим первопричину:

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

И это потому, что DraftObject определяется следующим образом:

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

и keyof не включает хорошо известные символы, такие как Symbol.toPrimitive (Проблема с TypeScript).

В качестве обходного пути вы можете разветвить типы immer и изменить определение Draft следующим образом:

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;

Или, если у вас не так много вхождений, просто добавьте утверждения типа в свой код по мере необходимости.

Вы были не одиноки! Immer был обновлен и, надеюсь, поможет вам именно в этой ситуации. Вот изменение:

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

Пока вы используете TypeScript 2.8.x или выше, все будет в порядке.

Это изменение действует с погружением 1.7.4.

У меня была аналогичная проблема сегодня, я нахожусь на immer 7.0.9. Посмотрел в документации, и они упоминают служебную функцию castDraft в качестве временного решения. https://immerjs.github.io/immer/docs/typescript#cast-utilities

Другие вопросы по теме