javascript

Как я ускорил TypeScript-типы в 15.7 раза

  • вторник, 18 августа 2026 г. в 00:00:04
https://habr.com/ru/articles/1071086/

Раз вы открыли эту статью, вы наверняка со мной согласны: иногда 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×

Достать S.Output (объект из 5 полей)

7842

501

15.7×

Достать S.Input (объект из 5 полей)

6767

501

13.5×

Достать S.Output (вложенный объект)

7732

501

15.4×

Описать + достать (объект из 10 полей)

9446

844

11.2×

Смёржить + достать output

13651

7039

1.9×

Заголовок — про те самые 15.7× на S.Output. Извлечение — самая частая операция: каждый type X = S.Output<typeof schema> в вашем коде платит именно за это.

Поехали. В рецепте три шага.

1. Сначала зафиксировать

Прежде чем что-то менять, нужен способ понять, что вы это не сломали. Для такой библиотеки это значит тесты на типы:

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 }, и никто вам об этом не сообщит. Ловят такое только тесты.

2. Померить до того, как что-то трогать

Если вы хотите что-то улучшить, это надо сначала померить — иначе потом не понять, улучшили вы хоть что-нибудь или нет.

Для этого я взял 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 — и любая будущая регрессия завалит сборку.

3. Тяжёлую работу отдать Claude

Третий шаг оказался самым простым: я попросил 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) и задавайте вопросы в комментариях!