具有扩展接口的打字稿可索引类型(TS2322)

时间:2019-01-24 09:44:29

标签: typescript

我正在通过重新实现我的流程类型原型来学习打字稿。这个问题使我有些困惑。

error TS2322: Type '(state: State, action: NumberAppendAction) => State' is not assignable to type 'Reducer'.
  Types of parameters 'action' and 'action' are incompatible.
    Type 'Action' is not assignable to type 'NumberAppendAction'.
      Types of property 'type' are incompatible.
        Type 'string' is not assignable to type '"number/append"'.

32   "number/append": numberReducer
     ~~~~~~~~~~~~~~~

  src/network/tmp.ts:13:3
    13   [key: string]: Reducer
         ~~~~~~~~~~~~~~~~~~~~~~
    The expected type comes from this index signature.

违规代码:

export interface State {
  sequence: number
  items: Array<any>
}

export interface Action {
  type: string
  payload: any
}

export type Reducer = (state: State, action: Action) => State;
export interface HandlerMap {
  [key: string]: Reducer
}

export interface NumberAppendAction extends Action {
  type: "number/append"
  payload: number
}

export const numberReducer = (state: State, action: NumberAppendAction) : State => {
  return {
    ...state,
    items: [
      ...state.items,
      action.payload
    ]
  }
}

export const handlers: HandlerMap = {
  "number/append": numberReducer
}

Reducer更改为:

export type Reducer = (state: State, action: any) => State;

解决了该问题,但是有关action参数的类型保证丢失了。

1 个答案:

答案 0 :(得分:2)

编译器警告您numberReducer不是Reducer,这是有充分理由的。 Reducer必须接受任何Action作为其第二个参数,但是numberReducer仅接受NumberAppendAction。这就像一个人宣传他的w​​alk狗服务,但只接受吉娃娃一样。尽管吉娃娃是狗,但那是虚假的广告。

这里的问题是类型安全性要求声明的类型中的function arguments be contravariantnot covariant。这意味着Reducer可以接受更宽的类型,而不能接受更窄的类型。 TypeScript通过TypeScript 2.6中引入的--strictFunctionTypes flag实施了此规则。

问题是要怎么做...您可以通过使用any或关闭--strictFunctionTypes来故意破坏类型安全性。我不建议这样做,但这是一个简单的出路。

类型安全的出路更加复杂。由于TypeScript不支持existential types,因此您不能轻易说出这样的话:“ HandlerMap是一种对象类型,其中每个属性都是 some 动作的简化器。键入A,并且该属性的键为A['type'](该操作的type属性)”。最接近的合理选择是使操作类型为A的类型为generic,希望我们能给编译器足够的提示以推断特定的{{1 }}或一组A类型。

这是一个可能的实现,其中有许多内联注释给出了其工作原理的草图。

A

我们可以对其进行测试:

// Reducer is now generic in the action type
type Reducer<A extends Action> = (state: State, action: A) => State;

// a HandlerMap is also generic in a union of action types, where each property
// is a reducer for an action type whose "type" is the same as the key "K" of the property
type HandlerMap<A extends Action> = {
  [K in A['type']]: Reducer<Extract<A, { type: K }>>
}

// when inferring a value of type `HM` that we hope to interpret as a HandlerMap<A>         
// for some A, we can make VerifyHandlerMap<HM>.  If HM is a valid HandlerMap, then
// VerifyHandlerMap<HM> evaluates to HM.  If HM is invalid for some property of key K, then
// VerifyHandlerMap<HM> for that key evaluates to the expected reducer type
type VerifyHandlerMap<HM extends HandlerMap<any>> = {
  [K in string & keyof HM]: (HM[K] extends Reducer<infer A> ?
    K extends A['type'] ? HM[K] : Reducer<{ type: K, payload: any }> : never);
}

// Given a valid HandlerMap HM<A>, get the A.  Note that the standard
// conditional type inference "HM extends HandlerMap<infer A> ? A : never" will
// not work here, A is nested too deepliy inside HandlerMap<A>.  So we manually
// break HM into keys and infer each A from each property and then union them
// together
type ActionFromHandlerMap<HM extends HandlerMap<any>> =
  { [K in keyof HM]: HM[K] extends Reducer<infer A> ? A : never }[keyof HM]

// the helper function asHandlerMap() will take a value we hope is a valid HandlerMap<A>
// for some A, verify that it is valid, and return a HandlerMap<A>.  
// If the type is *invalid*, the compiler should warn on the appropriate property.  
const asHandlerMap = <HM extends HandlerMap<any>>(hm: HM & VerifyHandlerMap<HM>):
  HandlerMap<ActionFromHandlerMap<HM>> => hm;

这样可以正常工作,并且根据需要推断const handlers = asHandlerMap({ "number/append": numberReducer }); // no error, handlers is of type HandlerMap<NumberAppendAction> 的类型为handlers

让我们介绍一个新的Handler<NumberAppendAction>来看看我们在犯错时如何得到警告:

Action

我们尝试一下:

interface Dog {
  breed: string,      
  bark(): void
}

interface DogWalkAction extends Action {
  type: "dog/walk",
  payload: Dog;
}

declare const dogWalkReducer: (state: State, action: DogWalkAction) => State;

糟糕,我输入了错字,const handlers = asHandlerMap({ "number/append": numberReducer, "dog/Walk": dogWalkReducer // error! //~~~~~~~~~~ <-- Type '"dog/Walk"' is not assignable to type '"dog/walk"'. }); 中的“ W”应为小写。让我们对其进行修复:

dog/Walk

这可行,并且所有内容都是类型安全的。正如我之前所说:复杂。还有其他可能的实现方式,但我不知道有什么既安全又简单的。是否选择类型安全性或简单性取决于您。无论如何,希望能有所帮助。祝你好运!