Skip to content
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

Allow constant strings as string literals for dynamic import statements. #32401

Open
5 tasks done
rjamesnw opened this issue Jul 15, 2019 · 8 comments
Open
5 tasks done
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@rjamesnw
Copy link

rjamesnw commented Jul 15, 2019

Search Terms

Use constant string in place of string literal for dynamic import statements.

Suggestion

Honestly I don’t know if this is a bug or a suggestion.

Is there a way to allow constant string to represent string literals? I’m thinking this should work, but it doesn’t. For example:

const moduleName = "../../../Project/src/Mod";

async function doSomething() {
    var mod = await import(moduleName); // (works only if a string literal at the moment)
}

In the case above, "mod" is of type "any" because the compiler doesn't recognize the string literal in the constant moduleName (for literal strings, the types are correctly pulled). I'm not sure if this was an oversight, but it makes sense to allow it, since constant strings cannot be reassigned. The only workaround is to wrap await import("../../../Project/src/Mod"); in a function:

async function getMod() { return import("../../../Project/src/Mod"); }
async function doSomething() {
    var mod = await getMod(); // (method required since const strings cannot be used to import module type)
}    

I may also add, it seems very difficult to import namespaces using dynamic imports, which I think is another terrible oversight.

async function doSomething() {
    var mod = await import("../../../Project/src/Mod"); // (forced to use a string literal to get typings)
    var o: mod.SomeClass; // ERROR, cannot find namespace 'mod'
    // var o: InstanceType<typeof mod.SomeClass>; // workaround1
    // var o: import("../../../Project/src/Mod").SomeClass; // workaround2 (who wants to keep typing full paths? A const string would be nice here.)
}

That doesn't even make sense. A namespace, while a type, is still a reference under the hood, and thus should still be importable dynamically somehow; perhaps like:

async function doSomething() {
    import mod = await import("../../../Project/src/Mod"); // (forced to use a string literal to get typings)
    var o: mod.SomeClass;
}

I think all this would aim to better support dynamic imports "on demand" instead of modules forcibly loading every single module when some may not be needed at all, It could also help promote faster initial page loads in many cases. ;)

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jul 15, 2019
@RyanCavanaugh
Copy link
Member

@rbuckton thoughts?

@rbuckton
Copy link
Member

Wouldn't this possibly involve needing to parse and rebind during check if the imported file wasn't previously part of the compilation? I could only see this work for files already included in the program.

@rbuckton
Copy link
Member

@RyanCavanaugh: Should we consider this for files included in the program? Would we need a distinct error message for files not included in the program (possibly only related to an implicit any)?

Usually the way I'd work around this today would be something like:

const id = 'foo';
const foo = (await import(id)) as typeof import('foo');

The only other approach I could see would be an extra step in processImportedModules in program to scan the SourceFile for import(x) and then scan for an x constant in scope before we even get to the binder.

@ajafff
Copy link
Contributor

ajafff commented Jul 15, 2019

scan for an x constant in scope before we even get to the binder.

You'd need to duplicate the whole logic for scopes, shadowing and symbol lookup. That becomes even more difficult if the import in within a namespace because that changes the lookup completely.

@rbuckton
Copy link
Member

Precisely, it wouldn't be 100% reliable.

@rjamesnw
Copy link
Author

rjamesnw commented Jul 15, 2019

@rbuckton I tried that also, but imagine having to do that 20 times in many files. It would be best to have a shared module, or global file, with constants to module paths. Using your example you would still have "../../../../very/long/paths" in 2 places (and scattered around code everywhere) - all of which must be updated when files move around, which is bad.

A better work-around might be:

const id = 'foo';
type TFoo = typeof import('foo');
const foo = (await import(id)) as TFoo;

Unfortunate extra work, but at least the path is only ever in 2 places in the whole app.

I might add, I'm trying to create an API for people to use, and I will be auto generating modules dynamically. So far, my best option is this:

abstract class Modules {
    static get CUSTOMMODULE() { return import("../../2677A76EE8A34818873FB0587B8C3108/shared/CUSTOMMODULE"); };
}

Then the end user in their code only has to do this:

var module = await Modules.CUSTOMMODULE;

Except I hit another wall where "module" is "not a valid namespace" for types:

var foo: module.foo;  // ERROR :(

(which I admit is another issue) Yes, there are workarounds (using InstanceType<typeof module.foo>), but it's a lot of unnecessary extra coding that should not have to be. This would be better:

import module = await Modules.CUSTOMMODULE;
var foo: module.foo;  // YAY :)

That is actually my biggest issue right now.

@ShanonJackson
Copy link

ShanonJackson commented Dec 24, 2019

Just to throw my use case in as well as i'd actually really like generics in import types.
I.E type importOf<T extends string> = typeof import(T);
This will allow incredible new possibilities for my isomorphic projects because i'll be able to do this...

const fetchUsers = api("./api/get-name")

const api<T extends string>(url: string): Promise<ReturnType<typeof import(T)["default"]>> => {
      return fetch(url.replace(".", "")
}

Where all my API endpoints export a default endpoint and which returns some data.
If anyone uses NextJS with typescript they'll know this would be an insanely useful feature because NextJS enforces file-path routing therefore all endpoints export a default and are from the same file as the url path.

@nevercast
Copy link

nevercast commented Jan 23, 2024

Seeing the new import stuff land in TypeScript 5.3 is awesome, I quickly scrolled through the change notes to see if const strings were usable in import statements, alas no such feature landed.

Would love to be able to use this as part of an RPC project that imports libraries as Workers. Currently it's something to the effect of:

function importWorker<T, F extends keyof T>(url: URL, workFunction: F) -> T[F]{ ... }
importWorker<typeof import('./worker.js'), 'worker'>(new URL('./worker.js'), 'worker')

It would be great to be able to use the following instead:

importWorker(new URL('./worker.js'), 'worker')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

6 participants