Раз вы открыли эту статью, вы наверняка со мной согласны: иногда 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> в вашем коде платит именно за это.
Поехали. В рецепте три шага.
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<…>, только чтобы достать обратно Outputtype 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) и задавайте вопросы в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/1071086/