Как я ускорил TypeScript-типы в 15.7 раза
- вторник, 18 августа 2026 г. в 00:00:04
Раз вы открыли эту статью, вы наверняка со мной согласны: иногда TypeScript компилируется мучительно долго.
С тем же самым столкнулся @_chenglou — крутой разработчик, на которого я равняюсь (он работал над React, Messenger, ReasonML и ReScript, а сейчас — над Midjourney и Pretext).
А ещё он пользуется Sury — самой быстрой библиотекой валидации схем, которую я поддерживаю, — и завёл вот этот issue: “Large TS types cause type inference slowdown”.
Ну… неприятно. Но я знал, что есть ArkType и что в системе типов TS люди собирали и не такое — вплоть до Doom. Значит, тормоза лечатся, а не заложены в фундамент. Ниже — рецепт, по которому я шёл. Его можно применить и к своей библиотеке.
Прежде чем начать — результаты. Это количество инстанцирований типов, измеренное через @ark/attest:
Операция | Было | Стало | Быстрее |
|---|---|---|---|
Описать объект из 10 полей | 1409 | 343 | 4.1× |
Описать объект из 5 полей (часть опциональных) | 17916 | 10557 | 1.7× |
Описать объект с вложенностью в 3 уровня | 31013 | 20419 | 1.5× |
Описать юнион из 5 объектов | 67580 | 53173 | 1.3× |
Достать | 7842 | 501 | 15.7× |
Достать | 6767 | 501 | 13.5× |
Достать | 7732 | 501 | 15.4× |
Описать + достать (объект из 10 полей) | 9446 | 844 | 11.2× |
Смёржить + достать output | 13651 | 7039 | 1.9× |
Заголовок — про те самые 15.7× на S.Output. Извлечение — самая частая операция: каждый type X = S.Output<typeof schema> в вашем коде платит именно за это.
Поехали. В рецепте три шага.
Прежде чем что-то менять, нужен способ понять, что вы это не сломали. Для такой библиотеки это значит тесты на типы:
import { expectTypeOf } from "vitest"; import * as S from "sury"; const user = S.schema({ id: S.string, age: S.number }); // Фиксируем выведенный тип. Поедет - тест упадёт. expectTypeOf<S.Output<typeof user>>().toEqualTypeOf<{ id: string; age: number; }>();
Раньше я брал для этого ts-expect, но переехал на тайп-тесты из Vitest (expectTypeOf выше), чтобы убрать лишнюю зависимость. Так или иначе, тесты у меня уже были — и, признаю, они себя полностью окупили. Оптимизация типов легко превращает { a: string } в { a?: string }, и никто вам об этом не сообщит. Ловят такое только тесты.
Если вы хотите что-то улучшить, это надо сначала померить — иначе потом не понять, улучшили вы хоть что-нибудь или нет.
Для этого я взял Attest (@ark/attest) от создателей ArkType. Меня очень вдохновляет их уровень владения TS, и, честно говоря, сам я в оптимизации на уровне типов не так силён. Хорошо, что силён в другом. 😄
Он считает, во сколько инстанцирований типов обходится выражение, и фиксирует это как бейзлайн:
import { bench } from "@ark/attest"; import * as S from "sury"; bench("define a user schema", () => { return S.schema({ id: S.string, age: S.number }); }).types([230, "instantiations"]);
Бонус: занесите эти бейзлайны в CI — и любая будущая регрессия завалит сборку.
Третий шаг оказался самым простым: я попросил Claude найти оптимизации, которые сильнее всего снизят число инстанцирований.
Фикс оказался крошечным. Чтобы достать тип из схемы, S.Output / S.Input матчились по всему типу Schema<…>. А тип этот здоровенный: большой объект с несколькими перегрузками with, пересечённый с юнионом всех вариантов схемы. То есть TypeScript разворачивал это целиком на каждое извлечение, и цена росла вместе со схемой:
// Было: матчим всю форму Schema<…>, только чтобы достать обратно Output type Output<T> = T extends Schema<infer Output, unknown> ? Output : never;
Но в каждой схеме Sury уже есть поле ~standard (маркер Standard Schema), в котором готовые типы и лежат. Так что достаточно заглянуть сразу туда, а не выводить всё заново из общей формы:
// Стало: матчим одно поле, в котором тип уже лежит type Output<T> = T extends { "~standard": { types?: { output: infer Output } } } ? Output : never;
Теперь извлечение стоит одинаково, какого бы размера ни была схема. Именно это изменение и даёт большую часть тех 11–16× из таблицы выше.
Зависит от ситуации (мой любимый ответ). Но скорость обратной связи значит всё больше — по вашему коду теперь итерируется ещё и AI. Если TypeScript в вашем проекте компилируется медленно, тем же способом можно найти медленные места и заняться ими.
Одна оговорка: 15.7× — это падение числа инстанцирований типов, а не секунд. Реальную компиляцию я не замерял. Но инстанцирования — это и есть та работа, которую tsc делает при проверке типов, так что их количество вполне честно отражает то, что вы чувствуете в редакторе. 🙂
Надеюсь, было полезно. Загляните в Sury — самую мощную библиотеку схем в экосистеме TS. Подписывайтесь на меня в X (@dzakh_dev) и задавайте вопросы в комментариях!