Skip to content

Включить проверку типов и отвергать неверно типизированные решения MyMap - #358

Merged
fey merged 3 commits into
mainfrom
fix/344-enable-type-checking
Aug 14, 2026
Merged

Включить проверку типов и отвергать неверно типизированные решения MyMap#358
fey merged 3 commits into
mainfrom
fix/344-enable-type-checking

Conversation

@fey

@fey fey commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes #344
Closes #268

Зачем

Проверка типов в курсе не выполнялась вообще. vitest стирает типы через esbuild, а tsc не вызывался нигде, поэтому expectTypeOf в 21 уроке и @ts-expect-error в 3 уроках проходили при любом содержимом урока. Из-за этого в уроке «Дженерики с несколькими параметрами» реализация MyMap с неверными типами принималась как верная (#268).

Что сделано

Инфраструктура (#344)

  • bin/test2.sh проверяет типы урока, который запускает. Именно этим скриптом платформа проверяет решение студента, поэтому это единственное место, где неверно типизированное решение можно отвергнуть.
  • make type-check (tsc --noEmit) покрывает весь курс и подвешен к make test, так что make check и CI получают быстрый агрегированный сигнал до 54 прогонов уроков. Добавлена обёртка compose-type-check.
  • README описывает, какой шаг за что отвечает и какие настройки tsconfig.json для этого обязательны.

Урок 47 (#268)

  • Второе инстанцирование несвязанной парой (MyMap<number, string[]>) ловит захардкоженные типы — так же, как это уже сделано в соседнем 20-generic-types. toEqualTypeOf вместо toExtend, потому что toExtend пропускал и отсутствующий undefined, и сплошной any.
  • {ru,en}/EXERCISE.md теперь называет то, что тест требует, а текст раньше только подразумевал: что тип должен работать с любым сочетанием параметров, что get() возвращает undefined для отсутствующего ключа и что Map лежит в свойстве values — последнее студенту было неоткуда угадать.

Попутно всплыло

Как только tsc реально запустился, обнаружились две скрытые ошибки конфигурации. moduleResolution: "node" — жёсткая ошибка в TypeScript 6, а TS 6 больше не подключает node_modules/@types автоматически, из-за чего отвалились ambient-типы Node, на которых держится урок hello-world. Плюс vitest.config.ts импортировал defineConfig из vite, где ключ test — лишнее свойство. Контент уроков править не понадобилось: все 21 урок с expectTypeOf и все 3 с @ts-expect-error оказались типо-чистыми.

Замечание к формулировке #344

Вариант 2 в issue описан неверно: там сказано, что обычный tsc --noEmit оставит expectTypeOf пустышкой. Матчеры expect-type проверяются исключительно на этапе компиляции (toExtend: <Expected extends …>(...MISMATCH: MismatchArgs<…>) => true), поэтому tsc их полноценно выполняет, а --typecheck у vitest меняет только формат отчёта. Отсюда выбор в пользу tsc.

Ограничение, которого не было в issue

Платформа запускает каждую проверку под timeout 6, а таймаут показывается студенту как зависшая программа с пустым выводом (lesson_tester.rb: exitstatus == 124failed-infinity, вывод обнуляется). Последовательный tsc по всему проекту давал 3–5 с сверх тестов и один раз из пяти пробивал лимит. Поэтому проверка ограничена одним уроком и идёт параллельно с vitest, а вывод буферизуется, чтобы ошибка типов докладывалась отдельно, а не под рантайм-выводом, который прошёл только потому, что типы стёрты.

Проверка

Точной командой платформы, с подменой index.ts через bind-mount (typescript 6.0.3, vitest 4.1.7):

решение результат
эталонное проходит
захардкоженные string/number (случай из #268) отвергается
захардкожен только ключ отвергается
get(key: K): V без undefined отвергается
any везде отвергается
переименован именованный экспорт в другом уроке, тест не тронут отвергается (критерий #344)

Также: рантайм-падение по-прежнему показывает студенту полный диф vitest; SIGTERM завершает раннер с кодом 143, не резюмируя выполнение.

Тайминги: прогон урока 1,3 с → 2,0 с (при лимите 6 с), make test 62 с → 95 с, make check зелёный за 2 м 36 с.

Не входит в этот PR

Ужесточение матчеров в остальных 20 уроках — вынесено в #357 с замерами: 28 из 30 эталонов такую замену переживают, но тексты заданий типы не фиксируют, поэтому нужна поурочная правка формулировок. Оба issue эту работу из объёма исключали.

fey and others added 3 commits August 5, 2026 21:46
vitest strips types with esbuild, so the type assertions in lesson tests were
never executed: expectTypeOf() in 21 lessons and @ts-expect-error in 3 lessons
passed no matter what the lesson exported. Nothing invoked tsc at all.

bin/test2.sh now type-checks the lesson it runs. That script, not make check, is
what the platform runs against a student's solution, so it is the only place
that can reject a wrongly typed one. It checks a single lesson and runs the
compiler concurrently with the tests, because the platform kills a lesson run
after six seconds; that keeps a run at about two seconds instead of four.
Output is buffered so a type error is reported on its own rather than under
runtime output that passed only because the types were erased.

make type-check covers the whole course in one pass and hangs off make test, so
CI gets a fast aggregate signal before the 54 per-lesson runs.

Two latent errors surfaced once tsc actually ran, both in config:
moduleResolution node10 is a hard error in TypeScript 6, and 6.0 no longer
enumerates node_modules/@types by default, which broke the ambient Node types
the hello-world lesson relies on. vitest.config.ts also had to import
defineConfig from vitest/config; via vite the test key is an excess property.
No lesson content needed fixing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#268)

The test only ever instantiated MyMap<string, number>, so an implementation that
ignored K and V and hardcoded string and number satisfied it — the case reported
in the issue. toExtend was also too loose to notice a get() that never returns
undefined, or a type built entirely out of any.

Instantiating the type a second time with an unrelated pair, number and
string[], is what catches hardcoding; it matches how the neighbouring
generic-types lesson already proves a type is generic. toEqualTypeOf replaces
toExtend so that a missing undefined and a blanket any are rejected too.

EXERCISE.md now states what the tightened test requires and previously only
implied: that the type has to work with any combination of the two parameters,
that get() yields undefined for a missing key, and that the Map lives in a
property named values — a student naming it otherwise had no way to know.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A trap that only cleaned up ran on INT and TERM and then let the script resume:
it deleted the logs while tsc and vitest were still writing, then read them back
empty and exited with vitest's status. On a local Ctrl-C that turned a killed run
into a silent pass, and made the script uninterruptible. Each signal handler now
exits, and cleanup also reaps a still-running tsc.

Also address review notes: an explicit mktemp template instead of the deprecated
-t, a compose-type-check wrapper to match the other checks, the tsconfig
settings the compiler steps depend on written down in README.md, and the
rationale kept in one place rather than repeated in three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fey
fey merged commit 31c7562 into main Aug 14, 2026
2 checks passed
@fey
fey deleted the fix/344-enable-type-checking branch August 14, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant