【问题标题】:Generics and Type Inference in TypeScriptTypeScript 中的泛型和类型推断
【发布时间】:2021-07-28 05:38:00
【问题描述】:

(跟进此question

我有一个函数接受带有一些状态定义的配置对象(它是一个状态机函数)。

例子:

const machine = createStateMachine({
  initial: 'inactive',
  states: stateNode({
    inactive: {
      on: { ACTIVATE: 'active' },
    }),
    active: stateNode({
      on: { DEACTIVATE: 'inactive' },
      effect(send) {
        send('DEACTIVATE') // Only accepts "DEACTIVATE".
      },
    }),
  },
});

用户可以通过调用send来切换当前状态,有两个地方可以使用:

  • 从配置中的effect 函数返回
  • createStateMachine 函数返回

我想从配置中尽可能多地推断,stateNode 是一个帮助类型推断的中间函数:

function stateNode<T extends Record<keyof T, PropertyKey>>({on, effect}: {
  on: T, effect?: (send: (action: keyof T) => void) => void}) {
  return { on, ...effect && { effect } }
}

到目前为止一切正常,但我想将“on”属性设为可选 - 有什么想法吗?

这是link to a TS Playground with the full code

【问题讨论】:

  • 我不明白...如果你省略了on,那么你也必须省略effect,对吧?因为send() 回调将采用never。但是你的状态节点只是{},所以你不必显式调用stateNode()。有点像this。这能如你所愿吗?还是我错过了什么。
  • 公平的问题,我应该更好地解释它:当状态机进入或离开该状态时,可以使用效果来运行任意代码。在包含转换事件的状态中,您还可以send 一个事件,但这并不是在效果中可以完成的全部。因此,从那个原始示例中,我可以冻结: stateNode({ effect(){ console.log("Entered Frozen") } } }),
  • 那么,像this 这样的东西?如果是这样,我会尝试写下答案(尽管我发现我还没有回答上一个问题)
  • 我已经准备好接受你对第一个的回答了,这个也很完美!我试图使用多个函数签名定义,但无法做到这一点。非常感谢您的所有帮助,如果有办法,我很想请您喝咖啡或啤酒;)
  • 唯一的事情是,现在我将never 作为createStateMachine 返回的函数的参数

标签: typescript typescript-generics


【解决方案1】:

从概念上讲,您需要做的就是在当前需要的任何地方将on 设为可选,并在您以编程方式使用on 类型的任何地方使用the NonNullable&lt;T&gt; utility type,这样就可以了:

type States<T extends Record<keyof T, { on?: any }>> = { [K in keyof T]: {
  on?: Record<keyof (NonNullable<T[K]['on']>), keyof T>,
  effect?(send: (action: keyof NonNullable<T[K]['on']>) => void): void;
} }

function createStateMachine<
  T extends States<T>>(states: {
    initial: keyof T,
    states: T
  }) {
  return function send(arg: KeysOfTransition<NonNullable<T[keyof T]['on']>>) {
  };
}

function stateNode<T extends Record<keyof T, PropertyKey>>({ on, effect }: {
  on?: T, effect?: (send: (action: keyof T) => void) => void
}) {
  return { ...on && { on }, ...effect && { effect } }
}

不幸的是,这并不完全奏效:

const a = createStateMachine({
  initial: 'inactive',
  states: {
    inactive: stateNode({
      on: {
        ACTIVATE: 'active'
      }
    }),
    frozen: stateNode({ effect() { console.log("Entered Frozen") } }), // error!
//  ~~~~~~
// Type 'Record<string | number | symbol, string | number | symbol>' is not assignable to 
// type 'Record<string | number | symbol, "inactive" | "active" | "frozen">'.
    active: stateNode({
      on: {
        DEACTIVATE: 'inactive',
      },
      effect: send => {
        send('DEACTIVATE')
      },
    })
  },
});
// const a: (arg: string | number | symbol) => void

当您使用具有on 属性的东西调用stateNode 时,编译器会正确推断T,但是当您忽略它时,编译器会推断Record&lt;PropertyKey, PropertyKey&gt;,而这根本不是您想要的。即使我可以编译,PropertyKey 也会让编译器忘记你所有的州名,你最终会得到像(arg: PropertyKey) =&gt; void 这样从createStateMachine() 出来的无用的东西。您希望 T 成为 undefined{}Record&lt;never, any&gt; 或其他东西。好吧,我尝试使用 stateNode 的单个调用签名,但上下文类型不断导致 T 被推断为这种不幸的宽类型。


相反,我建议将stateNode() 设为overloaded function,以便我们可以更好地控制在您省略on 属性时会发生什么:

function stateNode<T extends Record<keyof T, PropertyKey>>(param: {
  on: T;
  effect?: ((send: (action: keyof T) => void) => void) | undefined;
}): typeof param;
function stateNode(param: { effect?: () => void, on?: undefined }): typeof param;
function stateNode<T extends Record<keyof T, PropertyKey>>({ on, effect }: {
  on?: T, effect?: (send: (action: keyof T) => void) => void
}) {
  return { ...on && { on }, ...effect && { effect } }
}

如果在调用stateNode()时指定on,则使用第一个调用签名,行为与之前相同。如果您离开on,则使用第二个呼叫签名。这里根本没有T 类型可以推断;相反,编译器将 effect 视为可选的无参数函数(因为,正如您在 cmets 中所说,这就是您想要的)。在这两种情况下,输出与输入的类型相同。让我们看看这是否会有所改善:

const a = createStateMachine({
  initial: 'inactive',
  states: {
    inactive: stateNode({
      on: {
        ACTIVATE: 'active'
      }
    }),
    frozen: stateNode({ effect() { console.log("Entered Frozen") } }),
    active: stateNode({
      on: {
        DEACTIVATE: 'inactive',
      },
      effect: send => {
        send('DEACTIVATE')
      },
    })
  },
});
// const a: (arg: "ACTIVATE" | "DEACTIVATE") => void

看起来不错!

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-17
    • 2017-10-04
    • 2018-01-18
    • 2020-07-23
    • 2016-12-05
    • 1970-01-01
    • 2020-12-01
    • 2020-09-09
    相关资源
    最近更新 更多