lenec ru

← все посты

TypeScript 5.5 inference: что улучшилось

14K

TypeScript 5.5 принёс несколько улучшений в inference, которые я заметила в реальной работе через неделю после обновления. Это не громкий релиз, но для команды, которая пишет на типах как на документации, разница ощутимая. Расскажу о четырёх местах, где TS-команда подкрутила движок, и как это меняет повседневный код.

Inference для type predicates

Самое заметное изменение — TypeScript теперь умеет автоматически выводить type predicates у функций-фильтров. Раньше:

// 5.4 и раньше
const values = ['a', null, 'b', undefined, 'c'];

const filtered = values.filter((v) => v !== null && v !== undefined);
// тип filtered: (string | null | undefined)[]

Тип возврата — ровно тот же, что у входа. Чтобы получить чистый string[], приходилось писать руками:

const filtered = values.filter(
  (v): v is string => v !== null && v !== undefined,
);
// тип: string[]

В 5.5 этот type predicate выводится автоматически:

// 5.5
const filtered = values.filter((v) => v !== null && v !== undefined);
// тип: string[]

Условие: предикат должен быть достаточно простым, чтобы компилятор мог его проанализировать. v !== null, typeof v === 'string', v instanceof Date, 'foo' in v — все эти случаи теперь выводятся. Сложные условия с булевой логикой и собственными хелперами по-прежнему требуют явного v is X.

Для меня это убрало кучу шаблонного кода в фильтрах массивов. На больших проектах счёт идёт на десятки правок.

Контролируемая регрессия в narrowing

В 5.5 поправили старый баг с narrowing внутри замыканий. Раньше работало некорректно:

function process(data: { value?: string }) {
  if (data.value) {
    queueMicrotask(() => {
      console.log(data.value.toUpperCase()); // ошибка: object is possibly undefined
    });
  }
}

Внутри коллбэка тип data.value сужался до undefined, потому что между проверкой и обращением могла произойти любая мутация. Это поведение оставалось, потому что менять его было опасно.

В 5.5 поведение стало более предсказуемым в специфичных случаях: если переменная не пересваивается между проверкой и использованием, narrowing сохраняется. Полностью эту проблему не решили, но реже встречается.

Для перестраховки я по-прежнему делаю промежуточную переменную:

function process(data: { value?: string }) {
  const value = data.value;
  if (value) {
    queueMicrotask(() => {
      console.log(value.toUpperCase()); // OK
    });
  }
}

Регулярные выражения и сужение

В 5.5 добавили проверку синтаксиса regex в коде. До этого /foo[/g с незакрытой скобкой просто компилировался, и ошибка прилетала только в рантайме. Теперь компилятор замечает невалидные паттерны:

const re = /foo[/g; // ошибка компиляции в 5.5

Я сначала отнеслась скептически — кто же ошибается в простых regex? Но за месяц работы поймала два случая, когда коллеги забывали escapes в сложных паттернах с unicode. Полезно.

Изоляция модулей

Опция --isolatedDeclarations требует от каждого экспортируемого члена явных аннотаций типа. Это нужно для сборщиков, которые умеют генерировать .d.ts без полного TS-компилятора (Bun, esbuild).

// без isolatedDeclarations
export const get = (id: string) => api.get(id);

// с isolatedDeclarations
export const get = (id: string): Promise<User> => api.get(id);

На больших библиотеках это даёт 5-10x ускорение сборки type definitions. Для прикладного проекта эффект менее заметен, но если ты пишешь библиотеку — посмотри.

Условные типы и контекстная инференция

5.5 улучшил инференцию для условных типов внутри generic-функций:

function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

type Item = { id: number; name: string; tags: string[] };
const it: Item = { id: 1, name: 'a', tags: [] };

const id = pick(it, 'id'); // number
const name = pick(it, 'name'); // string

Раньше в более сложных случаях с условными типами компилятор «терял» точную инференцию и возвращал слишком широкий тип. В 5.5 такие ситуации стали реже. Я не могу привести один универсальный пример, но в нашем проекте после обновления исчезли пять-шесть «непонятных» any и unknown в местах, где раньше я расписывала overload руками.

Экспресс-апдейт: что стоит проверить после миграции

  1. Прогнать tsc --noEmit на всём проекте. Появятся новые ошибки в местах, где раньше типы были «обманчиво ок».
  2. Поискать filter(...), где явно указан type predicate — некоторые из них теперь избыточны.
  3. Проверить, что внутри функций-генераторов и async-функций вы не полагались на старое поведение narrowing в замыканиях.
  4. Убедиться, что регулярные выражения проходят новую проверку.

Что не изменилось

Есть ожидания, которые в 5.5 не сбылись:

  • Auto-complete для значений string-литералов в дженериках всё ещё ограничен.
  • Type-only imports/exports работают так же, как и раньше.
  • Декораторы остались в том же виде, что и в 5.4.
  • Performance компилятора стал чуть лучше, но без революции — на наших проектах сборка ускорилась на 4-7%.

Когда этого недостаточно

Если ваша работа упирается в edge-cases с нестандартными inference-сценариями, обновление 5.5 их не закроет. Я бы посоветовала смотреть в сторону:

  • type-fest — большой набор готовых утилитарных типов, в которых уже учтены тонкости.
  • tRPC — для API, где end-to-end типизация важнее, чем ручные ухищрения.
  • Прокладочные библиотеки на Zod / Valibot — они дают и валидацию, и точные типы.

В библиотечном коде стоит регулярно прогонять typescript-eslint с strict-конфигом и держать noUncheckedIndexedAccess: true. Эти настройки ловят больше багов, чем сами по себе обновления компилятора.

Что копать дальше

5.5 — спокойный апдейт без громких фич, но с приятными улучшениями инференции. Я бы рекомендовала обновляться без раздумий: ломающих изменений почти нет, а выигрыш по точности типов реальный. После апдейта пройдитесь по фильтрам в массивах и наслаждайтесь чистыми типами без v is X везде.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.