microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.33k stars 12.53k forks source link

Computed import types #44636

Open reverofevil opened 3 years ago

reverofevil commented 3 years ago

Suggestion

πŸ” Search Terms

import label:feature-request

βœ… Viability Checklist

My suggestion meets these guidelines:

⭐ Suggestion

Add ability to typeof import() a string literal type

πŸ“ƒ Motivating Example

declare module 'test' {
    export const test: number;
}

type GetExport<T extends string> = typeof import(T)

type Test = GetExport<'test'> // typeof import('test') β‰ˆ {test: number}

πŸ’» Use Cases

babel, eslint and webpack use package names in their API, dynamically require them and proxy data into their interface. Generally, API that looks like

doSomething({
    package: 'package-name',
    options: {
        optionForThisPackage: 'value',
    },
});

could greatly improve on typing with this.

Also it's often the case with dynamic imports that resulting Promise gets passed through several other methods (like React.lazy or loadable from @loadable/component). Currently the only way to get good types there is to have these duplicated at every import() site. It would be possible to assign some types to wrapper function in this case too.

type LazyRegistry<Packages extends Record<string, string>> = {
    [Name in keyof Packages]: Promise<(typeof import(Name))[Packages[Name]]>
}
const lazyRegistry = <Packages extends Record<string, string>>(packages: Packages): LazyRegistry<Packages> => {
    return Object.fromEntries(
        Object.entries(packages).map(([name, importName]) => {
            const lazy = React.lazy(async () => {
                return (await import(name))[importName];
            });
            return [key, lazy] as const;
        }),
    ) as LazyRegistry<Packages>;
};

type LoadableRegistry<Packages extends Record<string, string>> = {
    [Name in keyof Packages]: AsLoadableComponent<typeof import(Packages[Name])>
}
const loadableRegistry = <Packages extends Record<string, string>>(packages: Packages) => {
    return Object.fromEntries(
        Object.entries(packages).map(([name, importName]) => {
            return [key, loadable(() => import(importName))] as const;
        }),
    ) as LoadableRegistry<Packages>;
};

const DynamicComponentRegistry = someRegistry({
    Foo: 'Foo',
    Bar: 'Bar',
});
RyanCavanaugh commented 3 years ago

This is a pretty heavy lift from the architecture side since, during the typechecking phase, we generally don't go do new expensive I/O. We could potentially allow it for modules that are already in scope (probably pretty common) but if you already know the completely list of modules you might want to look up this way, it's pretty straightforward to write a helper type somewhere with that list instead of using typeof import at the use site

reverofevil commented 3 years ago

@RyanCavanaugh Doesn't typeof import() already do the same thing during typechecking phase?

The criteria "module has to be in scope" doesn't apply to babel, webpack and eslint, because neither of them imports modules from callee side at all.

If you let me add my subjective 2 cents, this feature is required to correctly type check at least 5 core frontend libraries, so it seems this is quite an important heavy lift to do.

jespertheend commented 2 years ago

My use case is that my library has support for mocking dynamic imports. Essentially the api looks like this:

const {mockedImport} = await importer.import("path/to/import.js");

The api closely mimics dynamic import() syntax. So it would be nice if the returned type from this function would return the same type as you would get when actually dynamically importing it. If this were supported the signature could look like:

async importWrapper<T extends string>(url: T) : Promise<typeof import(T)>

Right now I'm resorting to using the module itself as a generic parameter. But using it like this is kind of ugly:

const {mockedImport} = await importer.import<typeof import("path/to/import.js")>("path/to/import.js")
yarinsa commented 1 year ago

Having the same issue with vitest, this could be a nice feature!

import { vi } from "vitest";

export function importActual<T extends Parameters<typeof vi.importActual>>([path, ...args]: T): ReturnType<typeof import(`${T[0]}`)> {
    return vi.importActual(path, ...args);
}
robbiespeed commented 1 year ago

@RyanCavanaugh would it help if there was a new module primitive, such that any generic of type module is part of a first pass (before general type checking) which does all I/O. Then during type checking phase is performing a lookup on a table of all modules that were loaded during I/O phase.

It seems like something like this might be necessary if module expressions land. I imagine module expressions would be type module & Module (Module being the type for the class instance).

Usage could look like this:

function lazyImport <M extends module>(pathOrModule: M): () => typeof import(M) {
  return () => import(pathOrModule);
}
AliakseiMartsinkevich commented 1 year ago

To add one more use case, we use jest in out React application. And jest have ability to mock modules. To keep it typesafe we use the following pattern:

jest.mock(
  'path/to/module',
  (): typeof import('path/to/module') => ({  }),
);

If we could use generic import types it would make it less verbose and would remove duplication of path.

DerGernTod commented 1 year ago

same issue with jest and mocked functions. if you go the road and import the globals (instead of using ambient types), jest.SpiedFunction<T> expects a generic. if you combine this with isolateModules where you always want dynamic imports, you get types like this:

const myFnSpy = jest.SpiedFunction<typeof import("my-imported-module")["myImportedFunction"]>;

you can imagine this getting a bit verbose if you need a lot of spies in a test

testersen commented 10 months ago

When can we expect this to land?

imtaotao commented 7 months ago

What's the current status of this feature? Is there an expected support time?