获取动态 `import()` 的导入路径作为 TypeScript 字符串类型

bri*_*out 5 typescript

使用 TypeScript 可以实现以下功能吗?

const mod = await import('./path/to/module')
// Is it possible to implement a utility type "ImportPath" that extracts
// the import path? Here the type of modImportPath would be './path/to/module'.
type modImportPath = ImportPath<typeof mod>
Run Code Online (Sandbox Code Playgroud)

语境

我是vite-plugin-ssr的作者,我希望能够从以下内容推断页面 ID。

// The vite-plugin-ssr user provides the app's routes
const routes = [
  {
    // How to infer the page's ID `LandingPage` from this dynamic import?
    Page: () => import('./pages/LandingPage'),
    route: '/'
  },
  {
    // How to infer the page's ID `AboutPage` from this dynamic import?
    Page: () => import('./pages/AboutPage'),
    route: '/about'
  }
]
Run Code Online (Sandbox Code Playgroud)

Dim*_*ava 1

至少有 3 种不同的方法来实现此功能,但没有一种方法是完美的

declare const RawImportPath: unique symbol;
declare const PageId: unique symbol;

type SplitOffStart<T, V extends string> = T extends `${string}${V}${infer E}` ? E : T;
type SplitOffEnd<T, V extends string> = T extends `${infer E}${V}${string}` ? E : T;
type SplitGetLast<T, V extends string> = T extends `${string}${V}${infer E}` ? SplitGetLast<E, V> : T;
type GetName<Path extends string> = SplitOffEnd<SplitGetLast<Path, '/' | '\\'>, '.vue'>


function myImport<T extends string>(path: T) {
    let v = import(path)
    return v as typeof v & { [RawImportPath]: T, [PageId]: GetName<T> }
    // or `return v as Promise<{ [RawImportPath]: T, [PageId]: GetName<T>}>`
    // (`any` should be excluded as it would void types)
}
function addType<T extends string>() {
    // double-function is somewhat dirty but I don't know a better way
    return <V>(v: V) => v as V & { [RawImportPath]: T, [PageId]: GetName<T> }
}
type Imported<T extends string> = { [RawImportPath]: T, [PageId]: GetName<T> } & Promise<any>

const routes = [
    {
        /**
         * The import safety(file exists) typecheck is lost
         * The import suggestions are lost
         * Module type is lost
         * (property) Page: () => Promise<any> & {
         *     [RawImportPath]: "./pages/LandingPage.vue";
         *     [PageId]: "LandingPage";
         * }
         */
        Page: () => myImport('./pages/LandingPage.vue'),
        route: '/'
    },
    {
        /**
         * Looks dirty
         * No string sameness typecheck
         * (property) Page: () => Promise<typeof import('./pages/AboutPage')> & {
         *     [RawImportPath]: "./pages/AboutPage";
         *     [PageId]: "AboutPage";
         * }
         */
        Page: () => addType<'./pages/AboutPage'>()(import('./pages/AboutPage')),
        route: '/about'
    },
    {
        /**
         * No string sameness typecheck
         * Module type is lost
         * (property) Page: () => Promise<any> & {
         *     [RawImportPath]: "./pages/AboutPage";
         *     [PageId]: "AboutPage";
         * }
         */
        Page: () => import('./pages/ThirdPage') as Imported<'./pages/ThirdPage'>,
        route: '/about'
    }
]
Run Code Online (Sandbox Code Playgroud)

更好的方法被https://github.com/microsoft/TypeScript/issues/32705阻止