How to create type for all 3 possible argument definitions and call a function that expects either of those

144 views Asked by At

(types coming from @type/markdown-it) https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/markdown-it/lib/index.d.ts#L93

I'd like to create arguments types for the possible arguments to the markdown-it use method which is typed as this:

type PluginSimple = (md: MarkdownIt) => void;
type PluginWithOptions<T = any> = (md: MarkdownIt, options?: T) => void;
type PluginWithParams = (md: MarkdownIt, ...params: any[]) => void;

I have a react hook that I would like to hand in markdown-it plugins like this:

type Plugin = MarkdownIt.PluginSimple |
  [MarkdownIt.PluginWithOptions, {}?] | 
  [MarkdownIt.PLuginWithParams, ...any[]]
type Plugins = Plugin[]

function useMarkdown(source: any, plugins?: Plugins) {
  React.useEffect(() => {
    plugins?.forEach(plugin => md.use(...plugin))
  }, [md, plugins])
}

First of, I did not know how to add the template argument to the second plugin definition.

This does not work:

[<T = any>MarkdownIt.PluginWithOptions<T>, T?]

But mostly I would like the TS compiler to recognise that that the use of md.use(...plugin) is safe. It complains, that the argument needs to support

Expected at least 1 argument, but git 0 or more An argument for 'plugin' was not provided. Type Plugin must have a '[Symbol.iterator]()' method that returns an interator

Changing my line to handle the array cases manually:

plugins?.forEach(plugin => Array.isArray(plugin) ? md.use(...plugin) : md.use(plugin))

Removes the iterator error message but leaves the other too for the usage of ...plugin

1

There are 1 answers

0
Linda Paiste On

You are passing all three cases to md.use with spread syntax, so they all have to be tuples. In the case of PluginSimple, the arguments are a tuple of length 1.

type Plugin = [PluginSimple] |
  [PluginWithOptions, any] | 
  [PluginWithParams, ...any[]]
type Plugins = Plugin[]

Now for the error about requiring 1 or more arguments, we can make sure that typescript knows that there is always at least one argument by destructuring the first entry in the tuple separately from the rest. Based on our union type, we know that main is always defined and is one of the plugin types, while rest is any[] and will be [] for the simple plugin.

function useMarkdown(source: any, plugins: Plugins = []) {
  React.useEffect(() => {
    plugins.forEach(plugin => {
      const [main, ...rest] = plugin; 
      md.use(main, ...rest)
    })
  }, [md, plugins])
}

This isn't perfect because we aren't enforcing a match between the PluginWithOptions and its specific option type, but hopefully it's good enough for your use case. For more advanced typing, I would think about defining your own plugin object which holds a plugin and its arguments rather than using tuples.