TypeScript Immer类型的参数不能分配给DraftArray类型的参数

时间:2018-08-10 12:43:56

标签: typescript redux

我正在尝试将immer https://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 }

似乎我做了文档中的所有操作,但仍然收到错误。为什么会这样?

3 个答案:

答案 0 :(得分:1)

不幸的是,当对诸如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.toPrimitiveTypeScript issue)之类的众所周知的符号。

作为解决方法,您可以分叉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;

或者,如果发生的次数不多,只需在代码中添加类型断言即可。

答案 1 :(得分:1)

您并不孤单! Immer已更新,希望能在这种情况下为您提供帮助。这是更改:

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

只要您使用TypeScript 2.8.x或更高版本,就应该不错。

此更改自沉浸式1.7.4开始生效。

答案 2 :(得分:0)

我今天有一个类似的问题,我正在沉浸在7.0.9中。在文档中查找后,他们确实提到了实用程序功能castDraft作为解决方法。 https://immerjs.github.io/immer/docs/typescript#cast-utilities