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

String literal in typeof import in typescript 4.1.0-beta. #41550

Closed
ShanonJackson opened this issue Nov 16, 2020 · 6 comments
Closed

String literal in typeof import in typescript 4.1.0-beta. #41550

ShanonJackson opened this issue Nov 16, 2020 · 6 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@ShanonJackson
Copy link

TypeScript Version: 4.1.0-beta

Code

type T = "./api/hello";
type test = typeof import(`${T}`) // String literal expected.

Expected behavior:
No error.

Actual behavior:
Error

Playground Link:

The reason this is desirable is because i have a API of "typed" endpoints and i often do this.

// tsconfig.paths allows that import statement to resolve
// api here resolves the ReturnType of that import which is a function.
const response = await api<typeof import("/api/user")>("/api/user")

with generics + new string literal types in 4.1 i was hoping to achieve this.

// tsconfig.paths allows that import statement to resolve
const response = await api("/api/user") 
@DanielRosenwasser
Copy link
Member

My understanding is that in order to type-check the program, all program files need to be loaded to analyze each scope and make sense of the world. In order to do that, all import strings need to be known ahead of time. As a result, there unfortunately can't be a cycle between the type-checking and program construction phases.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Nov 16, 2020
@ShanonJackson
Copy link
Author

ShanonJackson commented Nov 17, 2020

If that's true how would .d.ts files work currently? because they are not referenced by any import statement but are implicitly in scope ? (Module augmentation)

All types MUST be known at compile time, otherwise TSC wouldn't work. Because all types are known at compile time surely we can just resolve the generics in import statements and load in those files as-well.

Hope i'm understanding TSC properly here.

@RyanCavanaugh
Copy link
Member

Program construction proceeds as follows:

  • Start with a list of initial files (from files or include)
  • Parse them for import or reference directives
  • Add those files to the mix
  • Repeat the last 2 steps until you stop finding new files
  • Bind each file to determine the global scope
  • Start typechecking

There is no going backwards in this algorithm - once you bring in a new file, you'd have to completely restart the typechecking process since you have no real way of knowing whether type data in that file invalidated previous typechecking outputs.

@ShanonJackson
Copy link
Author

ShanonJackson commented Dec 18, 2020

@RyanCavanaugh @DanielRosenwasser
I've been thinking about this 'design limitation' for along time, and I think I've got a modified proposal.

Types that can represent your file system.

Before:

const response = await api<typeof import("/api/user")>("/api/user")

After:

const response = await api.user(1);
// implementation
const api = new Proxy<FileSystem<"../../../api">>(...);

Semantics of 'FileSystem' (name yet to be finalized).

FileSystem traverses the files in the specified folder:
1: If that file is a module it adds it as a key at the depth position with its type set to its typeof import("...")
2: If that file is a folder it adds it as a object with keys to allow you traverse children modules

Because the end result of this type will just be a object it can easily be used in conjunction with existing 'Mapped' types etc.

More use cases:
1: Really strong support for image paths, and removing files in refactors that are referenced by string path.

 // (note: requires augmentation of JSX types);
<img src={"/static/dolphin.png"}/> // error dolphin.png isn't loaded in the file system.

2: Support for string based routing in applications and protection against routing refactors at a type-level.

 // (note: requires augmentation of JSX types);
<a href={"/home/name"}/>

3: Statically typed readFileSync for people using it to access their file system

const readFileSystem = (path: keyof FileSystem<".">): string=> fs.readFileSync(path);

4: Every use case where previously people had alot of types or classes floating around and then had a single place where they imported all those types/classes into a union just to support typescript can now just use a folder to store those things in conjunction with FileSystem

// people using ORM's can avoid having a 'single' source where all their classes are imported in favor of just using a folder.
await database.user(123);

// implementation 
class SomeORMClass {
    getUsers() {
        return [];
    }
}

// Replaced via FileSystem helper type.
type FileSystem = {
    users: {default: SomeORMClass} // representation of import("..") that has a default class export
}

type ORM = {
    [K in keyof FileSystem]: FileSystem[K]["default"]
}

declare const database: ORM ;
database.users.getUsers();

Further thinking but not neccessary:

  • People are going to want 'Flattened' keys in varying formats at a type-level, they can easily get these if they use new 4.1 recursive semantics with the new string literal types.

  • Mapped types in conjunction with new string template types opens up many other uses cases where a use case maps to something this is "FileSystem-Like" or is similar to the implementation of the FileSystem except with modifications.

  • Mapped types/String Literal types with FileSystem can be used to represent certain transformations that occur at build time at a type-level in your project.

Precedence: When people look at the implementation in the standard library they will notice FileSystem doesn't have a implementation in the same way you can "peek definition" for types like Exclude and Omit, however there already is precedence for type-level helpers that work as a 'compiler flag' (I.E ThisType<T>);

What would be your thoughts on this? In my mind this would be a complete game changer and solves so many more use cases not listed here:

FileSystem based Redux Action types.
FileSystem based ExpressJS (no need to import all routers into a single server.ts)
Type-Level support for changes that would occur to the file system during build time.

@ShanonJackson
Copy link
Author

Think this has legs so created a separate feature request, feel free to shoot it down there.
#42046

@phenomnomnominal
Copy link

I basically just want to be able to imbue a function with the same power that a require statement or import statement has.

I'd love to be able to do something like type CustomRequire<Module> = (id: string) => SomeModifier<Module> and let the compile know somehow that Module = typeof require(id);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants