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

Hay*_*yan 3 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>'.
Run Code Online (Sandbox Code Playgroud)

我有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'
}
Run Code Online (Sandbox Code Playgroud)

这是我的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 }
Run Code Online (Sandbox Code Playgroud)

看来我按照文档中的说明做了所有事情,但仍然收到错误。为什么会发生这种情况?

Mat*_*hen 6

不幸的是,当对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
      }
    }
  })
}
Run Code Online (Sandbox Code Playgroud)

然后你会发现问题出在 ,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>'.
Run Code Online (Sandbox Code Playgroud)

数组应该是协变的,为了支持这一点,第一个参数 是includes双变的,但不幸的是 TypeScript 猜测了错误的方向来报告失败。我们期望DraftObject<IBidding>可分配给IBidding,而不是相反。如果我们直接测试:

import { Draft } from 'immer'
import { IBidding } from './types'
let x: Draft<IBidding>;
let y: IBidding = x;
Run Code Online (Sandbox Code Playgroud)

然后我们终于看到了根本原因:

[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>'.
Run Code Online (Sandbox Code Playgroud)

这是因为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]>;
};
Run Code Online (Sandbox Code Playgroud)

并且keyof不包含众所周知的符号,例如Symbol.toPrimitiveTypeScript问题)。

作为解决方法,您可以分叉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;
Run Code Online (Sandbox Code Playgroud)

或者,如果出现的次数不多,只需根据需要向代码中添加类型断言即可。