tsc --noEmit чист, но приложение падает при старте из-за циклического импорта
Сервис собирается, проверка типов абсолютно чиста, но процесс умирает сразу при старте. entity.ts объявляет базовый класс, order.ts его наследует, а order-service.ts стоит между ними. Типы никто не менял — менялся только граф импортов.
Объясните, почему проверяющий типы молчит о цикле, фатальном в рантайме, назовите, что именно делает эмитируемый CommonJS в месте extends, и скажите, как разорвать цикл, не сваливая объявления типов в общую свалку.
$ node dist/index.js
/app/dist/order.js:4
class Order extends entity_1.Entity {
^
TypeError: Class extends value undefined is not a constructor or null
$ npx madge --circular src
1) src/entity.ts > src/order-service.ts > src/order.ts > src/entity.ts
$ npx tsc --noEmit
(no errors)
Определите причину.
Типы резолвятся лениво, поэтому циклическая ссылка на тип законна и проверка молчит. Значенческий цикл в эмитируемом JavaScript ей не виден: модуль, вычисляемый вторым, получает ещё пустой exports соседа, и entity_1.Entity в месте extends читается как undefined. Сотрите чисто типовые рёбра через import type, а общее значение вынесите в третий модуль.
- ✗Ждать, что
tscсообщит о цикле, который он по правилам вправе резолвить лениво - ✗Переставлять импорты во входном файле — это лишь меняет, кому достанется пустой
exports - ✗Сваливать все общие типы в один
types.tsвместо того, чтобы стереть чисто типовые рёбра
- →Почему
verbatimModuleSyntaxзаставляет этот класс багов проявляться раньше, а не позже? - →Когда значенческий цикл действительно неизбежен и как сделать его безопасным?
Почему проверка типов молчит
Проверяющий резолвит типы лениво: чтобы узнать форму Entity, ему не нужно «выполнять» entity.ts. Циклическая ссылка на тип для него полностью законна — и никакого диагноза не будет.
Рантайм устроен иначе. Эмитируемый CommonJS выполняет модули жадно и по порядку, и цикл выглядит так:
index.js → require('order-service')
→ require('order')
→ require('entity') // entity начинает выполняться
→ require('order-service') // УЖЕ в процессе → отдаётся пустой exports
Что происходит на extends
Пока модуль выполняется, его module.exports заполняется постепенно. Модуль, попавший в цикл вторым, получает объект-заготовку соседа: свойства ещё не присвоены. Поэтому entity_1.Entity — это undefined, и class Order extends undefined падает.
Починка
Все рёбра цикла здесь — типовые. Значит, их можно стереть:
// order-service.ts
import type { Entity } from './entity'; // стирается — ребра в рантайме больше нет
export function describe(e: Entity): string {
return e.id;
}
Правило: любое ребро, нужное только ради типа, пишется import type. Оставшийся значенческий цикл (если он есть) — настоящая проблема архитектуры: вынесите общее значение в третий модуль, который импортируют оба, и цикл превращается в дерево.