New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Design Spec] ESNext import() #14495

Closed
yuit opened this Issue Mar 6, 2017 · 31 comments

Comments

Projects
None yet
@yuit
Contributor

yuit commented Mar 6, 2017

import(specifier) is a newly proposed syntax for dynamically module loading. import(specifier) takes an assignment expression as an input (Note: unlike ES6 static import which only allow string literal as module specifier) and returns a promise of module namespace object.

The full proposal can be found here
Original issue: #12364

This issue will discuss design proposal for implementing import(): parsing, type-checking, and emitting.

The proposal will NOT include "module namespace object" type and expressing the shape of the loaded module (To be able to express such type will allow users to cast the result instead of "any" to be what they expected at runtime)...The issue will be discussed and implemented separately.

Parsing
import(specifier) will be parsed as CallExpression. and specifier will be parsed as AssignmentExpression

Error

  • Error if the module flag is ES2015.

  • Error if specifier is not assignable to string type

Type-checking

  • specifier is type checked as string type. (Clarification: the only limitation for specifier in original proposal is that it can called toString())

  • The return of import(specifier) will be as following
    -- if known module can be resolved from specifier, then we return Promise of module namespace object
    -- if not known module, then we return Promise of any. This will give an error in noImplicitAny mode and import() is used as an expression After offline discussion with @mhegazy @bowdenk7 @DanielRosenwasser, we think reporting an error without providing a way to easily get out of it (see #14844 for more detail) is undesirable. By casting to Promise<any> to satisfy the compiler will not provide users any benefit and arguably is a drawback because if in the future, there is a way to type the module easily, upgrading to use such syntax will not be convenient. Therefore, we will not issue an noImplicitAny error with this feature but with #14844 instead.

  • We will use existed module resolution logic to try to figure out the module of the dynamic import.

Emit
Emit for each different module kind emit is outlined below

import('blah');
  • Node
Promise.resolve().then(() => require('blah'));
  • AMD
define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    new Promise(resolve => require(['blah'], resolve));
});
  • UMD
(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    require.length === 1 ?
        /*CommonJs Require*/ Promise.resolve().then(() => require('blah'));
        /*Amd Require*/ new Promise(resolve => require(['blah'], resolve));
});
System.register([], function (_export, _context) {
  return {
    setters: [],
    execute: () => {
      _context.import('dynamic');
    }
  };
});
  • ES2018 new value
import('blah')

@yuit yuit self-assigned this Mar 6, 2017

@yuit yuit added this to the TypeScript 2.3 milestone Mar 6, 2017

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 7, 2017

import should not be allowed if --module ES2015.. we have no way of emitting them correctly.

@jods4

This comment has been minimized.

jods4 commented Mar 7, 2017

@mhegazy Maybe you should let import() passthrough when --module ES2015.
Likely scenario for me here is using a bundler like Webpack that will take care of it (as of webpack 2.2, import() is supported).

This behavior is useful and we need a way to generate output with import(). If you feel uncomfortable with labeling ES2015 something that really is an extension thereof, maybe we need a new ES201? target.

My interpretation is that ES2015 is the native, standard, ES module system and I'm not shocked to see the newly approved import() inside it, even if it was not strictly speaking in 2015.

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Mar 7, 2017

Shouldn't the 'if not known module' reflect the possibility of missing module and do not expose any?
I'm proposing in this case return Promise<{} | undefined>. Then the caller would be responsible to properly check result of the promise.

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Mar 7, 2017

Ah, sorry, forgot it would go to failure channel of the promise. Then it should be Promise<{}>.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 7, 2017

@mhegazy Maybe you should let import() passthrough when --module ES2015.
Likely scenario for me here is using a bundler like Webpack that will take care of it (as of webpack 2.2, import() is supported).

this is not valid ES2015 code. even if the engine supports ES2015 modules, import is a keyword and it would be an error.

I guess what you are looking for is a --module ES2018. (or may be --module esnext)

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 7, 2017

Then it should be Promise<{}>.

{} is fairly useless.. it will force users to case. any is more forgiving, and will allow users to proceed without adding a cast. the --noImplicitAny error would help those users interested in stomping out anys.

@Kovensky

This comment has been minimized.

Contributor

Kovensky commented Mar 7, 2017

The argument to import() is something that can be stringified (i.e. anything but symbol), not necessarily string. But allowing only string in the type checker makes sense.

The emit for Node is slightly incorrect. The require should only start after the stack has unwinded, but the example snippet does it synchronously. It should look like:

Promise.resolve().then(() => require('blah'))

This probably also applies to the other emits.

@Kovensky

This comment has been minimized.

Contributor

Kovensky commented Mar 7, 2017

@mhegazy the main (existing) implementer of import() is webpack, so it would be highly desirable to have a passthrough for it. Currently, TypeScript+webpack users are stuck with using the deprecated and, eventually, "worsly typed" System.import() method in webpack.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 7, 2017

Would --module esNext do? this will pass through import(..) as well as import .. and export ..?

@Kovensky

This comment has been minimized.

Contributor

Kovensky commented Mar 7, 2017

Yeah, that would definitely do.

@mohsen1

This comment has been minimized.

Contributor

mohsen1 commented Mar 7, 2017

If spec is scheduled for es2018 I think module=es2018 is better than esNext. Next is relative 😀

Also tracked at #12364

@jods4

This comment has been minimized.

jods4 commented Mar 7, 2017

@mhegazy I think whether you graft it to --module es2015 or you create a new --module es2018 doesn't matter much (I could argue for both), as long as we can use it. 👍

@bmeck

This comment has been minimized.

bmeck commented Mar 7, 2017

Note: import() needs to wait until the stack unwinds prior to doing any eval.

For Node it is also important to note that CJS modules only produce a default export according to the rewritten proposal:

Promise.resolve().then(() => {
  return {default: require('blah')}
});
@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 7, 2017

CJS modules returning a default is an orthogonal issue. it applies to all ES6 import forms as well. this is tracked under #8687, and should be done around the time the node ES6 module implementation is starting to solidify.

@yuit

This comment has been minimized.

Contributor

yuit commented Mar 8, 2017

Thanks all for the feedback. I have updated the spec accordingly. I decided that to introduce new flag since the syntax is not ES2015 valid

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 8, 2017

@yuit, For UMD modules, function.length is an ES5 feature, so i do not think we can use that. so we will have to use the boolean flag instead, or come up with another alternative.

@guybedford

This comment has been minimized.

Contributor

guybedford commented Mar 9, 2017

Just to mention here, the transform for System.register should be the following:

import x from 'x';
import('dynamic').then(...);
// ->
System.register(['x'], function (_export, _context) {
  var x;
  return {
    setters: [X => x = X.default],
    execute: () => {
      _context.import('dynamic').then(...);
    }
  };
});

This has been supported as of SystemJS 0.20 and is included in the Babel transformer.

@Kovensky

This comment has been minimized.

Contributor

Kovensky commented Mar 9, 2017

@mhegazy If you consider that there never was a pre-ES5 implementation of commonjs, it should be safe to rely specifically on require.length === 1. If it's undefined or otherwise different than 1, you're either running on ES3 (for which there are no commonjs implementations) or on AMD.

EDIT: oh, right, browserify on old IEs 😢

EDIT2: It's there since the first edition of ECMAScript, section 15.3.5.1 at the end of page 64. What browsers specifically would not have Function#length?

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 9, 2017

@Kovensky you are correct. i was wrong. function.length should be fine to use.

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Mar 13, 2017

Sorry for bringing this once again, but I didn't really get the reason why unresolved module will end up being any.
My understanding is that a dynamic module loading feature will be used in two major ways:

  1. module specified explicitly as a string literal (in form of module specifier in import from) or string constant expression (or, I'm not sure, as a union of string literal types)
    • in that case module can be resolved statically and TS can reason about this and give more specific type of the module like it does this for static module binding now
    • it has very limited set of usage scenarios (I can think of just module splitting with lazy loading)
  2. module specified as a dynamic expression which can be resulted in some string in runtime.
    • that case, to me, is going to be much common; it covers scenarios where modules are loaded as a result of some runtime module name calculation (plugins, configuration, use demand, ...)
    • it is common, that a module consumer expects a module with a specific contract and need to perform some duck-type checking to be sure that module is actually what was expected

My claim is that supplying unknown modules with type any would result in too permissive modules.
Moreover, with noImplicitAny there's no legal way to allow such unknown modules and not get "implicit any" error, isn't it? Having {} as a module type would allow type asserting in the way that module consumer needs without possibility to be poisoned by any.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 13, 2017

this is already the behavior today, a module that does not exist, but a .js file for it existed is resolved as any. i do not see why this is any different.

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Mar 13, 2017

I do see, actually. The behavior today assumes the static binding to be done where the module specifier is known at compile time. With import, that is by design to have a module specifier to not be known at compile time.

So, to be more practical, it comes up to a question: how can I avoid "implicit any" error under noImplicitAny with a code like this:

const module = await import(dynamicallyReceivedPluginModuleName);
@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 13, 2017

Ah, you should be able to cast to suppress the noImplicitAny error:

const module = await import(dynamic) as any;

Potentially we could allow import<T>(...) as sugar as well.

Another part of this change would be to enable some sort of syntactic form to express a module shape.. e.g. import (....) as moduleof "./blah". or import<moduleof "./blah">(...).

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Mar 13, 2017

ok, got it. It may work. Thanks for clarification.

@jods4

This comment has been minimized.

jods4 commented Mar 13, 2017

The following seems more natural?

const module: any = await import(dynamic);

That's how I fix "implicitely any" errors when they are intended: by adding the type declaration.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 13, 2017

if we assume it is a contextual-type-able then sure.

@saschanaz

This comment has been minimized.

Contributor

saschanaz commented Apr 18, 2017

I think using import('blah') with --outFile and --module systemjs should just emit System.import('blah') for migration like #12473. Is this already considered?

// older-namespace.ts
// namespace-based code which will eventually migrate to module-based code
namespace Foo {
  export async function foo() {
    const blah = await import('blah.js'); // should be System.import
  }
}

// blah.ts
// already migrated module
export function blah() {}
@olmobrutall

This comment has been minimized.

olmobrutall commented Jun 29, 2017

Hi guys,

I'm having problems using esnext as a replacement of commonjs that will preserve import for webpack.

This is the issue: #16820

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.