типы объединения потоков не работают в рекурсивном вызове

У меня возникли проблемы с потоком с проверкой типов пользовательского типа объединения, который позволяет Function, Promise или Array содержать либо false, либо экземпляр класса Plugin.

Код работает без проверки типов и позволяет разработчику вкладывать Plugin или false в любой другой разрешенный тип, однако я не могу заставить систему типов разрешать код без каких-либо ошибок.

Я работаю с предположением, что мы можем использовать рекурсивные типы, например type T = false | Array<T>, поэтому тип может быть либо ложным, либо массивом, содержащим другой тип 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/#0PTAEAEDMBsHsHcBQiAuBPADgU1LARgFZYDGKoAvKAN4C+qmOACtAK4DmAlgHYDOFioQaEgBDaDywDQAH1DN23KbIAUASgoA+Oa068loAIIAnIyLQAeebp4b9jI7AC2HCZZ3cbyYtBE8+V7mopLhFHLAAuUB4UI242KWJYXhiWUlgjZRCw9SopQRQACxcAOiycSjKpOjpEb18+AFEuXRxcwQwRU0c+SlopDHdeCmo6QUTko1SUdOUOrp5I-CJSHLzQQpK50J7QLe6q-o5sWcHIgN5VSKaWoKFQEFAAE1hQLlgN5tAOSF3BvhdhGIJGtvqATgouBRyJRROIsKs7oIjFgUCwjJCNjw1qMhFg4V8fsp0NhYD8BhCoZQAOSQFhcUgcJJUhGIzHFDBHLDg3RqVTYtYPNgo34UyAORxfXgcR44ERycUuSR3PESAlg8m6SXRET0rCk+VORUsu4a7jFQpYLjcwLkLRsjnHU1cVR8u40UACsAcFBYUw+3AAN19oCwImIBS+Pol+p1oE6pjQaxVOFBymMCeKLnTZmtzuNQidxUg6QaYYKuc06yKPHZnNzLv5dwe3G9HDEHAAXjhQ+GRbok-jU06tSgdcQ9T9zvn8tX2X8ANqFsoAXWGTsbQgeyNR6KrAOLRlA4ZELeaa23aIx1YONXGPFg0CwxTgbGUwSw8FA124XKor1CESgFSSRYFSNCukItbHFwH7aBCyhUk6AC0IHMhBghQVy84wZ+5wIchKDwLAzLLuhoCYco9iGhIxTIve0BBso2GwXhiGDEhhTIqBqikWRFFqJWOFwTybEQkhxZomhnpRAUsAsNAjxHiILCqnKMAIOsDAhiY6TSQAJA0AAe2CkA0OlGGsFEpPC0kAJKgER8mKdAHAANY4Ji6wvERRiuU8WBYAMaCvFg0RYIpgQ6sFYbjhgo54I+mnYNJFHzgJtoGs4NF0Q+jFpeoGXzlRWVPjlDFYUJrHIZAHBBiRPGkaRiB8kAA

Спасибо за любую помощь


person synthet1c    schedule 11.08.2018    source источник
comment
@ m0meni Спасибо за ваше предложение. Я думаю, что это сработает, но это противоречит цели наличия рекурсивных типов, в которых размещаются плагины. С помощью вышеизложенного вы можете вложить любой плагин любого типа.   -  person m0meni    schedule 11.08.2018
comment
Рекурсивные типы действительно разрешены. A demonstration with _1_: Try Flow example   -  person synthet1c    schedule 11.08.2018
comment
@synthet1c Я только что подал отчет об ошибке об ошибке вывода типа конструкции объекта, которую я описал в мой ответ.   -  person Rory O'Kane    schedule 13.08.2018
comment
Большое спасибо за ваш подробный ответ с примерами, советами по отладке и открытием проблемы с разработчиками для этой ошибки. Мне придется провести больше исследований в области инструментов потока и отладки, я прочитал документы, но до сих пор не понимаю некоторых концепций и не знаю, подходит ли для этого редактор vim. Я назначу награду, когда она будет доступна.   -  person Rory O'Kane    schedule 14.08.2018


Ответы (1)


Ошибка потока, из-за которой создание объекта приводит к any

You can spot one error if you run your type definition through the Prettier форматировщик кода:

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

Как видите, ваш тип Plugins верхнего уровня не принимает Array или Promise, потому что возвращаемое значение Plugins имеет более высокий приоритет, чем функция () => Plugins. You can fix that by adding parentheses around your () => Plugins тип функции:

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

Изменить: лучший обходной путь

new Engine(…) возвращает значение типа any вместо типа Engine, как вы ожидаете. Вот почему any никакие вызовы .pipe не выдают ошибки, даже если должны.

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

Вы можете обойти это, вручную аннотируя сконструированный движок как Engine, используя либо аннотацию типа, либо присвоение переменной:

В идеале вы могли бы заставить new Engine(…) возвращать Engine, но я думаю, что это ошибка в Flow, а не что-то, что можно исправить, изменив код. Я свидетельствую, что удаление следующего кода внутри вашего метода pipe также устраняет проблему:

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'))
    // …
)

Я создал задачу в системе отслеживания ошибок Flow по этому поводу , демонстрируя проблему на более минимальном примере. Для срабатывания ошибки требуется странная комбинация факторов; вам просто повезло, что вы встретили их всех.

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

Пока ошибка не будет исправлена, вы должны использовать обходной путь добавления аннотации типа.

Как упоминал wchargin, в проблеме GitHub, вы также можете обойти эту проблему, явно аннотируя constructor Engine как возвращающее void:

Окончательный код

Если вы сделаете это, вам не придется добавлять аннотацию Engine в каждое место, где вы создаете объект Engine.

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

Combine these two fixes to get your final, working code (in Try Flow):

Я играл с ним некоторое время, и не мог понять это. Однако я думаю, что более простым/лучшим решением было бы просто создать функцию канала для каждого типа, т.е. pipe, pipeFunction, pipePromise, pipeArray и т. д.

Проблема приоритета

// @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')])]])])
)
person Rory O'Kane    schedule 14.08.2018
comment
That’s a step forward, but when you примените это изменение к исходному коду, вы увидите, что _10_ теперь не выдает нужную ошибку. Using the feature of Try Flow to identify the type of the element under the cursor, I found why this happens by adding some test code near the bottom: - person synthet1c; 14.08.2018