Типизация динамического import() со спецификатором, известным лишь в рантайме
loadPlugin загружает модуль плагина по имени в рантайме. При вычисляемом спецификаторе компилятор не может отрезолвить ни один файл, поэтому await import(...) отдаёт any, и любое обращение к свойству после этого не проверяется — отсутствующий или неверный плагин падает молча, глубоко внутри вызывающего кода.
Ограничения: ничто типа any не должно вытечь из функции, форму модуля надо проверить в рантайме (система типов ничего не знает о файле, выбранном строкой), и приводить модуль к Plugin без такой проверки нельзя. На несоответствующий модуль — бросайте исключение.
export interface Plugin {
name: string;
run(input: string): string;
}
// Загружает ./plugins/<name>.js в рантайме.
export async function loadPlugin(name: string): Promise<Plugin> {
// ваш код здесь
}
Допишите реализацию.
С литеральным спецификатором import('./m.js') типизирован — компилятор резолвит файл и выводит Promise<typeof import('./m.js')>. С вычисляемым резолвить нечего, поэтому результат — any, и всё дальше не проверяется. Ожидайте в unknown, сузьте type guard-ом, который смотрит форму в рантайме, и бросайте исключение, если он не прошёл.
- ✗Считать, что спецификатор из шаблонной строки всё ещё резолвится в типизированный модуль
- ✗Приводить импортированный модуль к
Pluginбез рантайм-проверки его формы - ✗Позволять
anyиз вычисляемогоimport()вытечь наружу через тип возврата
- →Что именно обозначает
typeof import('./m.js')и когда это можно написать руками? - →Как сохранить типобезопасность реестра плагинов, если плагинам разрешено регистрировать себя самим?
Почему тип теряется
import() с литеральным спецификатором компилятор резолвит как обычный импорт:
const mod = await import('./plugins/upper.js');
// ^? typeof import('./plugins/upper.js')
Но спецификатор из шаблонной строки — ./plugins/ плюс name плюс .js — вычисляемый. Файла, на который можно посмотреть, нет, и результат становится any. Дальше любое обращение к свойству молча проходит проверку.
Решение
Ожидаем в unknown, а форму проверяем type guard-ом:
export interface Plugin {
name: string;
run(input: string): string;
}
function isPlugin(value: unknown): value is Plugin {
if (typeof value !== 'object' || value === null) return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.name === 'string' && typeof candidate.run === 'function';
}
export async function loadPlugin(name: string): Promise<Plugin> {
const mod: unknown = await import(`./plugins/${name}.js`);
const exported = (mod as { default?: unknown }).default;
if (!isPlugin(exported)) {
throw new Error(`Plugin "${name}" has no valid default export`);
}
return exported;
}
mod: unknown — ключевая строка: она гасит any прямо на границе, и без сужения ничего сделать нельзя. isPlugin — единственное место, где утверждается форма, и утверждается она на основании рантайм-проверки, а не на веру.