RFC: Dynamic imports in ReScript

Let’s discuss how dynamic import can best be supported in ReScript. Before discussing a potential implementation, let’s try to find a definition for what a dynamic import is, what it means to ReScript, and then map out all of the use cases we want to support. This is a continuation of this GitHub issue: https://github.com/rescript-lang/rescript-compiler/issues/5593

Worth noting that dynamic imports are possible to do already in ReScript, but they need lots of manual and error prone plumbing and aren’t very safe.

What is a dynamic import?

import - JavaScript | MDN ← this is the general definition, and what we’ll want to compile to in the end. Essentially, it’s a function that takes a string pointing to a JS file containing ES modules. That function then returns a promise that resolves to the full file as an object with all exports from the file on it.

In TS, you do dynamic imports just like in JS, so import("./path/to/file.js"). But TS can infer the type of the file you’re pointing to, which means that the import will be typed correctly. This leads to the experience being essentially:

const fileJs = await import("./some/file.js");
fileJs.someFunc();

What could dynamic import mean in ReScript?

What we want for dynamic imports in ReScript is probably a bit different to the JS/TS version. We don’t want to have to care about files and file names when we’re coding, since ReScript only have us caring about what modules are accessible in the current scope. So, ideally we focus on how to dynamically import a module, without needing to know the path of the generated file for that module.

Open ended question; What does this mean for file level modules vs nested modules? File level modules, as in “this file is a module”, is the only thing that’s going to map 1:1 to how JS does it - importing an entire file. But whether a module is at the file level or not isn’t necessarily anything you need to think about today when using ReScript.

Use cases

Here’s a non exhaustive list of use cases for dynamic imports. Please feel free to add to this thread so we cover as much ground as possible.

Dynamically importing a module

This is the standard use case, dynamically importing module X and then using exports from it. How it looks in JS:

const fileJs = await import("./some/file.js");
fileJs.someFunc();

React.lazy

This is a React specific API that expects to be fed a function that dynamically imports a module with a React component as the default export. It’s a bit tricky because we want to preserve the props type from the React component. How it looks in JS:

const MyLazyComponent = React.lazy(() => import("./some/MyComponent.js"));

<MyLazyComponent />

Discussion

Let’s start here and discuss what dynamic imports could mean in ReScript, and what we want to support. Eager to hear your thoughts and reflections.

11 Likes

I’ve never seen this in practice, but probably worth noting, dynamic imports might be used to bring some ESM-modules (e.g. an ES6 lib) to the scope of CJS-modules (e.g., a ReScript project transpiled to Common JS).

Supporting this case could require something new around external and @module declarations.

Perhaps we should show the current way to do it in ReScript and discuss why that is not good enough?

1 Like

This sort of thing is how I’ve been doing it. Not every module will need the .default addition but this one does.

I restrict the types to my use case, but it would be quite easy to stick a 'a in there and make it able to pretend it’s importing whatever you say it is.

type tinymceGlobal
type tinymce = {default: tinymceGlobal}
@val
external importModule: string => Js.Promise.t<tinymce> =
  "import"

let loadTinyMCE = () =>
  importModule("tinymce") |> Js.Promise.then_(moduleCode => {
    Js.log2("imported value", moduleCode.default)
    Js.Promise.resolve(moduleCode.default)
  })

This code is a bit unweildy, but that’s mostly because I have to use Js.Promise in the playground (I don’t use it in my app). I think it’s fine otherwise.

1 Like