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] Syntax to represent module namespace object in dynamic import #14844

Closed
yuit opened this Issue Mar 24, 2017 · 18 comments

Comments

Projects
None yet
@yuit
Contributor

yuit commented Mar 24, 2017

Another half of dynamic import #14774 is to enable users to be able to describe the shape of the module namespace object return by dynamic import. This is needed because

  • TypeScript currently can only resolve module if the specifier is a string literal (recall syntax of dynamic import import(specifier)) and Promise<any> will be returned...Therefore we need a way for users to be able to specify the module shape in type argument of the Promise and escape noImplicitAny (Note if dynamic import is used as expression statement. we won't issue noImplicitAny)

  • When we emit declaration file, we need a syntax to indicate that this is a Promise of some module. Currently compiler will give an error as reference external module is not the same as the module of containing file (or lack of one)

🚲 🏠 There are two ares which need to be discuss:

  1. Syntax to bring in the dynamic loaded module so compiler can load during compile time
  2. Syntax to specifier the return value of dynamic load (e.g just as a cast, or type argument to dynamic import etc.)

Proposes for (1) - bring in the module (This assume that we will use casting syntax but it doesn't have)

  1. use namespace import, (this will get elided anyway if just used as type)
import * as MyModule1 from "./MyModule1"
import * as MyModule12 from "./MyModule2"
var p = import(somecondition ? "./MyModule1" : "./MyModule2") as Promise<typeof MyModule1 | typeof MyModule2>;
  1. introduce new keyword ,moduleof....when collecting module reference we will also collect from moduleof as well
var p = import(somecondition ? "./MyModule1" : "./MyModule2") as Promise<moduleof "./MyModule1" | moduleof "./MyModule2">;

Proposes for (2) - indicate the return type

  1. Since dynamic import is parsed as call expression, we can reused the mechanism of specifying type argument
var p = import<Promise<moduleof "./MyModule1" | moduleof "./MyModule2">>(somecondition ? "./MyModule1" : "./MyModule2");
  1. Contextual Type
var p: <Promise<moduleof "./MyModule1" | moduleof "./MyModule2">> = import(somecondition ? "./MyModule1" : "./MyModule2");
  1. Just use casting

  2. allow all of the above.

Design meeting update #14853
In addition to be able to simply refer to shape of module namespace object, it is also desirable to use as a QualifiedName (e.g var x: <some syntax to indicate that we refer to module object>.namespace.interface) and to have syntax that can easily allow users to be able to do so is crucial.

Candidates we are currently considered:

declare var m: module("./foo").namespace.Interface   // this one seems to be the most popular
declare var m: (moduleof "./module").namespace.Interface;
declare var m: (importof "./module").namespace.Interface;
declare var m: (module "./module").namespace.Interface;

I will want to discuss some performance/incremental parsing related to this area and see if we think it will be desirable to consider top-level type import like import type * as ns from "./a.js".

@cevek

This comment has been minimized.

cevek commented Mar 24, 2017

Why not just like?

var p = import<typeof MyModule1 | typeof MyModule2>(somecondition ? "./MyModule1" : "./MyModule2");
@yuit

This comment has been minimized.

Contributor

yuit commented Mar 24, 2017

@cevek two things with that syntax: 1) it won't be clear that the output is a Promise of typeof Module1 or typeof Module2 or typeof Module1 or typeof Module2 2) during design meeting we decided to go with using casting syntax and allow contextual type because as you can see the type arguments syntax get pretty ugly with import<Promise<moduleof MyModule1 | moduleof MyModule2>>

I will update the proposal with what we discuss during design meeting

@rozzzly

This comment has been minimized.

rozzzly commented Mar 27, 2017

So looking at the proposed snippets above:

import * as MyModule1 from "./MyModule1";
import * as MyModule12 from "./MyModule2";
var p = import(somecondition ? "./MyModule1" : "./MyModule2") as Promise<typeof MyModule1 | typeof MyModule2>;
var p = import(somecondition ? "./MyModule1" : "./MyModule2") as Promise<moduleof MyModule1 | moduleof MyModule2>;

I don't see how typeof and moduleof differ... Moreover, in the second snippet, where does the MyModule1 identifier come from? Is it just imported (like by import * as MyModule1 from "./MyModule1";) or is there some entirely distinct mechanicism I'm not picking up on?

My hope is that this is some huge typo and that the moduleof './MyModule1' syntax that @mhegazy suggested in #14495 is what you meant.

@yuit

This comment has been minimized.

Contributor

yuit commented Mar 27, 2017

@rozzzly it is a typo for this one.... Copy and paste mistake here 😭 I have updated the original post.

@yuit yuit self-assigned this Mar 27, 2017

@yuit

This comment has been minimized.

Contributor

yuit commented Mar 28, 2017

Update

  • The top candidate for syntax for describing shape of module is module(...)
  • nuance semantic : should the syntax bring in type of module as well as namespace?
// 0.ts
export interface foo {}
export class C {}
// 1.ts
var p: module("./0");  
  1. is module(...) bring both type of module and namespace so you can do p.foo as well as p.C ?

  2. or is module(...) only bring namespace side and therefore to get type of module one will have to do var anotherP: typeof module("./0");

If we go with (2.) then for dynamic import, to express the shape of import module will be

var d = import(blah)  as Promise<typeof module("blah")>;  // very verbose

Another syntax that comes up for using with dynamic import is

var d = import<"blah">(blah);
var d = import(<"blah">blah);

In both casting syntax, type argument must be string literal.,

Conclusion

  • Possible use short hand to specify shape of dynamic import separately like
var d = import<"blah">(blah);
var d = import(<"blah">blah);
  • May be being able to bring just type of module should be treat in other issues so we can discuss syntax and semantic in details. So we won't have to worry about how such syntax plays with dynamic import (e.g. it may be a long verbose syntax like var d = import(blah) as Promise<typeof module("blah")>; )
@yuit

This comment has been minimized.

Contributor

yuit commented Mar 28, 2017

This is a subset of this larger issue #13231

@yuit yuit added this to the Future milestone Mar 28, 2017

@weswigham

This comment has been minimized.

Member

weswigham commented Mar 28, 2017

I definitely like the type operator for looking up modules, since it would probably be nice if import could just be a function(like) in the appropriate lib.d.ts typed like

declare function import<T extends string>(path: T): Promise<typeof module(T)>;

where module(T) behaves like the type indexing operator with respect to unions of string literal types, but looks up the symbol associated with the module that string type indicates, rather than indexing off a specific type (and behaved like it has an indexer to an implicit any to catch cases where a string literal could not be inferred). I would almost like to use square brackets for module like a map, except that the string literal needs to be treated as a path and resolved from the containing file (probably? configuration dependent?), and that transformation makes it more like a function than a straight map. The new syntax also simultaneously allows one to type require and any other module-loader-specific lookup in a similar way to import, which would be excellent.

Oh, and this is unrelated but oddly unsatisfying: The import spec as currently written allows something like this:

export default (x) => import(x);

But not this:

export default import;

since it's not really a function.
This seems.... bad. Unnecessarily confusing. If the engine can record where the call-site for the ImportCall is, I don't (as not a JS-engine-author) see why it couldn't just add ImportKeyword to the list of expressions and make its reference (a la a getter, but in the global scope) return an annotated importing function keyed to the file the reference was in. But that's not about the TS syntax for looking up modules.

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 29, 2017

Unfortunately we can not do the general type operator; given the way the compiler is architected today all file-system interactions happen at the very beginning when we are collecting files to compile; whereas resolving types happens at a later stage where new files are not expected to be added, nor are file-system operations expected. so it has to be module("literal") and not module(T).

@rbuckton rbuckton referenced this issue Apr 13, 2017

Merged

[Master] wip-dynamic import #14774

4 of 5 tasks complete
@phiresky

This comment has been minimized.

phiresky commented Jul 13, 2017

wouldn't the typeof module("foo") syntax conflict with the potential syntax for arbitrary typeof expressions (#6606)?

e.g.

declare function foo(a: boolean): string;
type Q = typeof foo(true); // Q is string
@CallMeLaNN

This comment has been minimized.

CallMeLaNN commented Feb 14, 2018

What is the final syntax?

A module instance is required to avoid repeating the same import('module') and deal with await/Promise every time a module reference is needed. Even though the module loaded only once, it require the calling function to be async or wrap the bottom one with .then().

I came across where there is a conditional import to only add hardware specific module, load the module to get an initialization side effect and doing some cleanup before exit. Currently I keep a variable of required function within the module instead of the module itself.

In my opinion

The top candidate for syntax for describing shape of module is module(...)

  1. module(...) is awesome.

nuance semantic : should the syntax bring in type of module as well as namespace?

  1. I vote for the module(...) not to bring in type but just namespace to make it similar with import * as a from "a" because module(...) may refer to default property.
var p: typeof module("a");
p = import<typeof module("a")>(a);
type moduleA = typeof module("a");
  1. I like implicit type parameter above (without the Promise) instead of casting because it is well documented in IntelliSense instead of casting from any. Any concern to have something like function import<T>(module: string): Promise<T>?

I prefer to avoid typeof Module because it require import * as Module from "Module", the static import may produce side effect.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Mar 9, 2018

This seems like something that this feature request of mine could solve quite simply...

TL;DR: it'd reify types as pseudo-properties (transparent to the runtime) and allow them to be passed around and defined like so. Support for this would fall out fairly naturally.

However, it might potentially be a little cumbersome due to the later binding of types.

@mhegazy mhegazy unassigned yuit Mar 9, 2018

@mhegazy

This comment has been minimized.

Contributor

mhegazy commented Mar 9, 2018

Discussed this in #22445, and conclusion is to go with import(<StringLiteral>)

@felixfbecker

This comment has been minimized.

felixfbecker commented Mar 20, 2018

Sorry if I'm missing something, but what is wrong with this, which is already possible?

import * as _foo from './foo'

let foo: typeof _foo

Also want to point to this piece from the DT "common mistakes" readme why import() shouldn't get a type parameter that I strongly agree with:

getMeAT<T>(): T:
  If a type parameter does not appear in the types of any parameters, you don't really have a generic function, you just have a disguised type assertion.
  Prefer to use a real type assertion, e.g. getMeAT() as number.
  Example where a type parameter is acceptable: function id<T>(value: T): T;.
  Example where it is not acceptable: function parseJson<T>(json: string): T;.
  Exception: new Map<string, number>() is OK.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Mar 21, 2018

@felixfbecker The string isn't a type parameter. import(...) in JS is a call-like syntactic expression much like super(...). For similar reasons, you can't do list.map(import) or list.map(super).

@felixfbecker

This comment has been minimized.

felixfbecker commented Mar 21, 2018

@isiahmeadows I am aware - like import is call-like, this proposal definitely looks type-parameter-like, so the argument still applies:

var p = import<Promise<moduleof "./MyModule1" | moduleof "./MyModule2">>(somecondition ? "./MyModule1" : "./MyModule2");
@weswigham

This comment has been minimized.

Member

weswigham commented Mar 21, 2018

@felixfbecker why would you not just cast there?

@felixfbecker

This comment has been minimized.

felixfbecker commented Mar 21, 2018

@weswigham that's my point

@clshortfuse

This comment has been minimized.

clshortfuse commented May 9, 2018

This seems to not work with export default class

foo.js

export default class Foo {
  constructor() {
    this.name = 'bar';
  }
}

Fails: Property 'name' does not exist on type 'typeof import("/test/foo")'.

/** @typedef {typeof import('./foo')} Foo */

/**
 * @param {Foo} foo
 * @return {string}
 */
function getName(foo) {
  return foo.name;
}

If I change foo.js to use export class Foo {, then /** @typedef {import('./foo').Foo} Foo */ works fine.

EDIT: Nevermind. I have to use /** @typedef {typeof import('./foo').default} Foo */

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