简体   繁体   English

如何在接口中使用 typescript 泛型类型?

[英]How to use typescript generic type in interface?

export const createModel = <
  T,
  R = {
    [propName: string]: <P>(prevState: T, payload: P) => T;
  }
>(model: {
  state: T;
  reducers: R;
  effects: (dispatch: {
    [K in keyof R]: (
      payload: R[K] extends (prevState: T, payload: infer P) => T ? P : never
    ) => T
  }) => Record<string, () => Promise<void>>;
}) => model;


const model = createModel<string[]>({
  state: ['foo'],
  reducers: {
    update(prevState, payload: string[]) {
      return [
        ...prevState,
        ...payload
      ]
    }
  },
  effects: dispatch => ({
    async request() {
      dispatch.update(['bar'])
      // (payload: unknown) => string[]
    }
  })
})

But I got:但我得到了:

TS2322: Type '<P>(prevState: string[], payload: string[]) => string[]'
is not assignable to type '<P>(prevState: string[], payload: P) => string[]'

How can I make the attribute function of dispatch get the correct payload type.如何使调度的属性 function 获得正确的有效负载类型。 Maybe it will work like Vue.defineComponent() .也许它会像Vue.defineComponent()一样工作。

By the way, any articles or books can learn typescript in depth?对了,有什么文章或书籍可以深入学习typescript?

Conceptually, your createModel() should have the following type:从概念上讲,您的createModel()应具有以下类型:

export const createModel = <T, P extends object>(
  model:
    {
      state: T,
      reducers: {
        [K in keyof P]: (prevState: T, payload: P[K]) => T
      },
      effects: (dispatch: {
        [K in keyof P]: (
          payload: P[K]
        ) => T
      }) => Record<string, () => Promise<void>>
    }
) => model;

Here, T corresponds to the type of state , while P corresponds to the mapping from keys in reducers and effects to payload types.这里, T对应于state的类型,而P对应于从reducers中的键和effects到有效负载类型的映射。 This is very straightforward and clean, but unfortunately the compiler really can't infer P from call sites:这非常简单明了,但不幸的是,编译器确实无法从调用站点推断P

// T inferred as string[], 
// P inferred as object
const badModel = createModel({
  state: ["foo"], reducers: {
    update(prevState, payload: string[]) {
      return [
        ...prevState,
        ...payload
      ]
    }
  }, effects: dispatch => ({
    async request() {
      dispatch.update(['bar']) // error!
      // ----> ~~~~~~ <-- update not present on object
      // 
    }
  })
});

Here the type T is correctly inferred as string[] , but the compiler is unable to use reducers or effects to infer the keys of P , and the compiler ends up just falling back to the object constraint.这里类型T被正确推断为string[] ,但编译器无法使用reducerseffects来推断P的键,编译器最终只是退回到object约束。 And so you get errors.所以你会得到错误。

If you don't mind manually specifying type parameters when you call createModel() , then things will work out:如果您不介意在调用createModel()时手动指定类型参数,那么事情就会解决:

const model = createModel<string[], { update: string[] }>({
  state: ["foo"], reducers: {
    update(prevState, payload) {
      return [
        ...prevState,
        ...payload
      ]
    }
  }, effects: dispatch => ({
    async request() {
      dispatch.update(['bar']) // okay
      // 
    }
  })
});

So that's one way to proceed.所以这是一种方法。


But if you don't want to write out redundant information (eg, a third instance of update and unnecessarily mentioning string[] for T ), then you need to care about TypeScript's type parameter and contextual type inference algorithm, and its limitations.但是,如果您不想写出冗余信息(例如, update的第三个实例和不必要地提及Tstring[] ),那么您需要关心 TypeScript 的类型参数和上下文类型推断算法及其局限性。

Roughly, the compiler takes a certain small number of inference "passes" where it tries to use information to determine candidates for generic type parameter types (like P ), or for the type of an unannotated callback parameter (like prevState ).粗略地说,编译器采用一定数量的推理“通过”,它尝试使用信息来确定泛型类型参数类型(如P )或未注释回调参数类型(如prevState )的候选者。 It plugs in those candidates and checks again to try to infer more things.它插入这些候选者并再次检查以尝试推断出更多的东西。 But it's very easy for the compiler to run out of inference passes before inferring everything you care about, and it gives up and falls back to some general type like unknown or whatever type a parameter is constrained to.但是编译器很容易在推断出您关心的所有内容之前用完推断传递,并且它放弃并回退到某些通用类型,例如unknown或参数约束的任何类型。 See microsoft/TypeScript#25826 , microsoft/TypeScript#29791 , and microsoft/TypeScript#38872 for examples.有关示例,请参见microsoft/TypeScript#25826microsoft/TypeScript#29791microsoft/TypeScript#38872

For now, this is just a limitation of the language.目前,这只是语言的限制。 There is a suggesstion at microsoft/TypeScript#30134 for the compiler to use a more complete unification algorithm for type inference, but who knows if anything like that will ever happen. microsoft/TypeScript#30134建议编译器使用更完整的统一算法进行类型推断,但谁知道是否会发生类似的事情。 It's best to just work with the system we have.最好只使用我们现有的系统。


The most straightforward way around this problem is to split the inference job into manageable chunks, where each chunk needs just one or two things inferred, and then refactor so that the compiler more or less has to infer in that order.解决这个问题最直接的方法是将推理作业拆分为可管理的块,其中每个块只需要推断一两个事物,然后重构,以便编译器或多或少地必须按该顺序进行推理。 In this case, we can take your single generic function and split it into a curried function where each subsequent call adds in a bit more to the model :在这种情况下,我们可以将您的单个通用 function 拆分为一个咖喱function,其中每个后续调用都会增加一点model

export const createModel = <T,>(state: T) =>
  <R extends Record<keyof R, (prevState: T, payload: any) => T>>(reducers: R) =>
    (effects: (dispatch: {
      [K in keyof R]: (
        payload: R[K] extends (prevState: any, payload: infer P) => any ? P : never
      ) => T
    }) => Record<string, () => Promise<void>>) => ({ state, reducers, effects });

Note that it's not always possible to infer a type like P from a value of a mapped type that depends on P but is not P itself.请注意,并非总是可以从依赖于P但不是P本身的映射类型的值推断出像P这样的类型。 It is much easier to infer a type like R from a value of type R itself.从类型R本身的值推断像R这样的类型要容易得多。 This means we have to calculate our payload types from our reducer types, using conditional type inference similar to your original code.这意味着我们必须使用类似于原始代码的条件类型推断从我们的 reducer 类型计算我们的有效负载类型。 It's uglier, but it helps with inference.它更丑陋,但它有助于推理。

Anyway, the order of operations above is: First we ask for state and infer T from it.无论如何,上面的操作顺序是:首先我们要求state并从中推断出T Then we ask for reducers and infer R from it, which has already been constrained to an object type that depends on the already-inferred T .然后我们要求reducers并从中推断R ,它已经被限制为依赖于已经推断的T的 object 类型。 This should also allow the compiler to contextually infer the type of prevState .这也应该允许编译器根据上下文推断prevState的类型。 And then we ask for effects , whose type should already be completely known since T and R fix it completely.然后我们要求effects ,其类型应该已经完全知道,因为TR完全修复了它。

Let's try it:让我们尝试一下:

const model = createModel(["foo"])({
  update(prevState, payload: string[]) {
    return [
      ...prevState,
      ...payload
    ]
  }
})(dispatch => ({
  async request() {
    dispatch.update(['bar'])
  }
}));

/* const model: {
    state: string[];
    reducers: {
        update(prevState: string[], payload: string[]): string[];
    };
    effects: (dispatch: {
        update: (payload: string[]) => string[];
    }) => Record<string, () => Promise<void>>;
} */

Looks good.看起来不错。 The compiler infers T as string[] and R as the correct type for reducers , and then effects is also properly typed.编译器将T推断为string[]并将R推断为reducers的正确类型,然后effects也正确键入。


It's up to you whether manually specifying types is more or less annoying than writing complexly-typed curried functions.手动指定类型是否比编写复杂类型的柯里化函数更烦人,这取决于您。

Playground link to code Playground 代码链接

You likely want P to extend T :您可能希望P 扩展T

export const createModel = <
  T,
  R = {
    [propName: string]: <P extends T>(prevState: T, payload: P) => T;
    //                     ^^^^^^^^^ add this
  }
>(model: {
// ...
}) => model

This way, regardless of the previous state, you force the next state ( payload ) to match that type.这样,无论之前的 state 是什么,您都强制下一个 state ( payload ) 匹配该类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM