镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

[Design] Syntax to represent module namespace object in dynamic import #14844

Description

@yuit

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".

Activity

  1. cevek commented on Mar 24, 2017

    @cevek

    Why not just like?

    var p = import<typeof MyModule1 | typeof MyModule2>(somecondition ? "./MyModule1" : "./MyModule2");
  2. yuit commented on Mar 24, 2017

    @yuit
    ContributorAuthor

    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

  3. rozzzly commented on Mar 27, 2017

    @rozzzly

    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 Mohamed Hegazy (@mhegazy) suggested in #14495 is what you meant.

  4. yuit commented on Mar 27, 2017

    @yuit
    ContributorAuthor

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

  5. self-assigned this
    on Mar 27, 2017
  6. yuit commented on Mar 28, 2017

    @yuit
    ContributorAuthor

    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")>; )
  7. yuit commented on Mar 28, 2017

    @yuit
    ContributorAuthor

    This is a subset of this larger issue #13231

  8. added this to the milestone on Mar 28, 2017
  9. weswigham commented on Mar 28, 2017

    @weswigham
    Member

    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.

  10. mhegazy commented on Mar 29, 2017

    @mhegazy
    Contributor

    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).

  11. phiresky commented on Jul 13, 2017

    @phiresky

    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
  12. CallMeLaNN commented on Feb 14, 2018

    @CallMeLaNN

    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.

  13. dead-claudia commented on Mar 9, 2018

    @dead-claudia

    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.

  14. added
    SuggestionAn idea for TypeScript
    and removed
    DiscussionIssues which may not have code impact
    on Mar 9, 2018
  15. modified the milestones: , TypeScript 2.9 on Mar 9, 2018
  16. mhegazy commented on Mar 9, 2018

    @mhegazy
    Contributor

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

  17. felixfbecker commented on Mar 20, 2018

    @felixfbecker
    Contributor

    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.

  18. dead-claudia commented on Mar 21, 2018

    @dead-claudia

    Felix Becker (@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).

  19. felixfbecker commented on Mar 21, 2018

    @felixfbecker
    Contributor

    @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");
  20. weswigham commented on Mar 21, 2018

    @weswigham
    Member

    Felix Becker (@felixfbecker) why would you not just cast there?

  21. felixfbecker commented on Mar 21, 2018

    @felixfbecker
    Contributor
  22. clshortfuse commented on May 9, 2018

    @clshortfuse

    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 /** zouying (@typedef) {import('./foo').Foo} Foo */ works fine.

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

  23. locked and limited conversation to collaborators on Jul 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions