在高阶函数中使用 TypeScript 中的参数从字典中选择回调

问题描述 投票:0回答:1

我有一个函数,它接受一个回调对象和一个具有与回调对象的键匹配的

type
属性的输入对象。 该函数使用输入对象调用匹配的回调。 我想创建一个高阶函数,它接受回调对象并返回一个接受匹配输入对象的函数。 但是,当我调用返回的函数时,我收到 TypeScript 错误,该错误指出输入对象不可分配给 never 类型。

type Actor <K extends string, T> = (input: { type: K } & T) => void
type Actors <K extends string, T> = { [Key in K]: Actor<K, T> }
type TfromA<K extends string, A extends Actors<K, any>> =
  A extends Actors<K, infer T> ? T : never

export function marion<
  K extends string,
  A extends Actors<K, TfromA<K, A>>,
> (
  actors: A,
  input: { type: K } & TfromA<K, A>
): void {
  const actor = actors[input.type]
  actor(input)
}

interface Alpha { type: 'alpha', count: number }
interface Beta { type: 'beta', label: string }
const alphaBeta = {
  alpha: (input: Alpha) => console.log(input.count),
  beta: (input: Beta) => console.log(input.label)
}
const alpha: Alpha = { type: 'alpha', count: 42 }
marion(alphaBeta, alpha) // No errors

function higher<
  K extends string,
  A extends Actors<K, TfromA<K, A>>,
> (
  actors: A
): (input: TfromA<K, A> & { type: K }) => void {
  return function (input: TfromA<K, A> & { type: K }): void {
    marion(actors, input)
  }
}
const lower = higher(alphaBeta)
lower(alpha)
// Argument of type 'Alpha' is not assignable to parameter of type 'never'.
// The intersection 'Alpha & Beta & { type: string; }' was reduced to 'never' because property 'type' has conflicting types in some constituents.ts(2345)

如何创建一个可重用的函数来处理任何匹配的回调和输入对象集,并可用于创建更高阶的函数?

游乐场:https://tsplay.dev/w2844m

typescript
1个回答
0
投票

问题中的代码存在一些问题。让我们看看一个有效的版本,它是如何工作的,以及它与问题中的版本有何不同:

type Input<K, V> = { type: K } & V
type Actor<K, V> = (input: Input<K, V>) => void
type Actors<T extends object> =
  { [K in keyof T]: Actor<K, T[K]> }

export function marion<T extends object, K extends keyof T>(
  actors: Actors<T>, input: Input<K, T[K]>
): void {
  const actor = actors[input.type]
  actor(input)
}

function higher<T extends object>(
  actors: Actors<T>
): <K extends keyof T>(input: Input<K, T[K]>) => void {
  return input => {
    marion(actors, input)
  }
}

我发现将

Input<K, V>
的类型别名指定为
{type: K} & V
很方便,因为该类型会出现多次。
Actor<K, V>
类型基本相同,但
Actors
映射类型 仅在您要转换的对象类型 T 中才是
generic
。键
K
处的每个属性都是
Actor<K, T[K]>
(其中
T[K]
是键 T
K
属性的
value
,这就是我使用
V
作为类型参数的原因)。

然后,

marion()
在该对象类型
T
和特定输入键
K
中是通用的。当您调用
marion()
时,TypeScript 可以从
T
类型的
actors
参数推断出
Actors<T>
。您无需显式编写
TfromA
实用程序类型即可使其工作。 请注意,
actors[input.type]
属于
Actors<T>[K]
类型,其计算结果为
Actor<K, T[K]>
,并且由于
input
属于
Input<K, T[K]>
类型,因此您可以调用
actors[input.type](input)

现在

higher()
函数很容易编写,因为它只是 marion()
curried
版本。它接受类型为
actors
的参数
Actors<T>
,因此只需在
T
中通用即可。 然后它返回另一个泛型函数,该函数具有类型参数
K

当您用示例调用它时,所有这些都有效:

marion(alphaBeta, alpha); // okay
marion(gammaDelta, gamma); // okay
const lower = higher(alphaBeta); // okay
lower(alpha) // okay

您的原始版本不起作用的原因:

  • Actors<K extends string, T>
    类型没有帮助,因为每个
    { [Key in K]: Actor<K, T> }
    意味着每个属性都是
    Actor<K, T>
    类型,即使
    K
    结果是键的union,我们也不能确定提前
    K
    将会是什么。通过使其成为
    T
    的每个属性的映射类型,我们处于有利位置,在检查
    K
    参数之前不必担心
    input

  • 你的

    TfromA<K, A>
    对于TypeScript来说很难进行一般性的推理。它是作为条件类型实现的,因此在函数
    marion
    higher
    中,TypeScript 不知道
    TfromA<K, A>
    实际上是什么。由于 TypeScript 本身知道如何从
    T
    推断
    A
    ,因此我们无需担心这一点。

  • 您的

    higher()
    函数在
    K
    A
    中是通用的,但它不需要
    K
    类型的输入。相反,它只需要
    A
    类型的输入。虽然
    A
    constrained 到与
    K
    相关的类型,但 TypeScript 无法从约束推断类型参数(请参阅 microsoft/TypeScript#7234)。而且您甚至不想在那里推断
    K
    ,因为在调用返回的函数之前您不会知道
    K
    。 所以
    K
    无法推断,退回到
    string
    ,然后你得到
    TfromA<string, typeof alphaBeta>
    ,它只是
    never
    ,因为
    Actors<K, T>
    将所有属性都扔进一个包里。 这使得
    higher()
    返回
    (input: never) => void
    ,这本质上是不可调用的。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.