首页 > 解决方案 > 流联合类型在递归调用中不起作用

问题描述

我在对自定义联合类型进行类型检查时遇到问题,该类型允许一个FunctionPromise或者Array包含false一个类的一个或一个实例Plugin

该代码无需类型检查即可工作,并允许开发人员将 a Pluginor嵌套false在任何其他允许的类型中,但是,我似乎无法让类型系统允许代码没有任何错误。

我正在假设我们可以使用递归类型,例如type T = false | Array<T>,该类型可以是 false 或包含另一种T类型的数组。我在 github 评论中读到递归类型或允许,但是找不到代码示例。

// @flow

type object = {}
type Plugins =
    false
  | Plugin
  | () => Plugins
  | Array<Plugins>
  | Promise<Plugins>

class Plugin {
  name: string
  constructor(name) {
    this.name = name
  }
}

class Engine {
  params = {}
  plugins = {}
  constructor(params: object) {
    this.params = params
  }
  pipe(plugin: Plugins): Engine {
    // do nothing if plugins is false
    if (plugin === false) {
      return this 
    }
    else if (typeof plugin === 'function') {
      this.pipe(plugin())
    }
    // get plugin from inside a Promise
    else if (plugin instanceof Promise) {
      plugin.then(plugin => this.pipe(plugin))
    } 
    // iterate over each item of an array
    else if (Array.isArray(plugin)) {
      plugin.forEach(plugin => this.pipe(plugin))
    }
    // initialize each plugin
    else if (plugin instanceof Plugin) {
      this.plugins[plugin.name] = plugin
    }
    // return this for chaining
    return this
  }
}

const engine = new Engine({ name: 'one'})
    .pipe(new Plugin('plugin-one'))
    .pipe([new Plugin('plugin-two')])
    .pipe(Promise.resolve([new Plugin('plugin-three')]))
    .pipe(() => new Plugin('plugin-four'))
    // should cause a flow type error
    .pipe(true)
    // I would like this to work deeply nested in any acceptable type
    .pipe([() => Promise.resolve([() => [Promise.resolve([new Plugin('plugin-five')])]])])

setTimeout(function() {
  console.log(engine)
}, 20)
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>

我尝试了多种不同的类型定义,并取得了不同程度的成功,但是,它们通常会修复警告,但不再捕获类型错误。

这是试图将最终类型与包装类型分开

type PluginTypes = false | Plugin
type Plugins = 
    Array<PluginTypes> 
  | Promise<PluginTypes> 
  | () => PluginTypes

这将通过添加Plugins到联合类型来防止错误,但是,类型检查似乎不再起作用。

type Plugins = 
    false
  | Plugin
  | Plugins
  | Array<Plugins> 
  | Promise<Plugins> 
  | () => Plugins

https://flow.org/try/#example

谢谢你的帮助

标签: javascriptflowtypeunion-types

解决方案


优先级问题

如果您通过Prettier代码格式化程序运行类型定义,您可能会发现一个错误:

type Plugins =
  | false
  | Plugin
  | (() => Plugins | Array<Plugins> | Promise<Plugins>)

如您所见,您的顶级Plugins类型不会接受 anArray或 a Promise,因为返回值Plugins优先级高于 function () => Plugins。您可以通过在函数类型周围添加括号来解决此问题:() => Plugins

type Plugins =
  | false
  | Plugin
  | (() => Plugins)
  | Array<Plugins>
  | Promise<Plugins>

对象构造导致的流程错误any

这是向前迈出的一步,但是当您将此更改应用于原始代码时,您会看到.pipe(true)现在无法抛出所需的错误。使用 Try Flow 的特性来识别光标下元素的类型,我通过在底部附近添加一些测试代码找到了为什么会发生这种情况:

const initialEngine = new Engine({ name: "one" });
// In Try Flow, put the cursor within `initialEngine`. The type is `any`.

new Engine(…) 返回一个类型的值,any而不是Engine你期望的类型。这any就是为什么没有调用.pipe之后会抛出错误的原因,即使它们应该如此。

您可以通过使用类型注释或变量赋值手动将构造的引擎注释为来解决此问题:Engine

console.log(
  (new Engine({ name: 'one'}): Engine)
    .pipe(new Plugin('plugin-one'))
    // …
)
const initialEngine: Engine = new Engine({ name: 'one'})

console.log(
  initialEngine
    .pipe(new Plugin('plugin-one'))
    // …
)

理想情况下,您可以new Engine(…)返回一个Engine,但我认为这是 Flow 中的一个错误,而不是通过更改代码来解决的问题。我的证据是,在您的方法中删除以下代码pipe也可以解决问题:

    // get plugin from inside a Promise
    else if (plugin instanceof Promise) {
      plugin.then(plugin => this.pipe(plugin))
    } 

在 Flow 的 bug tracker 上创建了一个关于此的问题,用一个更简单的示例演示了这个问题。这个错误需要一个奇怪的特定因素组合来触发;你遇到他们只是你的运气。

在修复错误之前,您应该使用添加类型注释的解决方法。

编辑:更好的解决方法

正如GitHub 问题中提到的wchargin ,您还可以通过将's显式注释为返回来解决此问题:Engineconstructorvoid

class Engine {
  // …
  constructor(params: object): void {
    this.params = params
  }
  // …
}

如果这样做,您将不必在构造对象Engine的每个地方添加注释。Engine

最终代码

结合这两个修复以获得最终的工作代码(在 Try Flow 中):

// @flow

type object = {}
type Plugins =
    false
  | Plugin
  | (() => Plugins)
  | Array<Plugins>
  | Promise<Plugins>

class Plugin {
  name: string
  constructor(name) {
    this.name = name
  }
}

class Engine {
  params = {}
  plugins = {}

  // type the constructor as returning `void` to work around
  // https://github.com/facebook/flow/issues/6738
  constructor(params: object): void {
    this.params = params
  }

  pipe(plugin: Plugins): Engine {
    // do nothing if plugins is false
    if (plugin === false) {
      return this
    }
    else if (typeof plugin === 'function') {
      this.pipe(plugin())
    }
    // get plugin from inside a Promise
    else if (plugin instanceof Promise) {
      plugin.then(plugin => this.pipe(plugin))
    } 
    // iterate over each item of an array
    else if (Array.isArray(plugin)) {
      plugin.forEach(plugin => this.pipe(plugin))
    }
    // initialize each plugin
    else if (plugin instanceof Plugin) {
      this.plugins[plugin.name] = plugin
    }
    // return this for chaining
    return this
  }
}

console.log(
  new Engine({ name: "one" })
    .pipe(new Plugin('plugin-one'))
    .pipe([new Plugin('plugin-two')])
    .pipe(Promise.resolve([new Plugin('plugin-three')]))
    .pipe(() => new Plugin('plugin-four'))
    // should cause a Flow type error
    // $ExpectError
    .pipe(true)
    // The following deeply-nested parameter now works
    .pipe([() => Promise.resolve([() => [Promise.resolve([new Plugin('plugin-five')])]])])
)

推荐阅读