Эта статья — перевод оригинальной статьи «Announcing TypeScript 5.6».

Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Сегодня мы рады объявить о выходе TypeScript 5.6!

Если вы не знакомы с TypeScript, то это язык, созданный на основе JavaScript и добавляющий синтаксис для типов. Типы описывают формы, которые мы ожидаем от наших переменных, параметров и функций, а программа проверки типов TypeScript помогает выявить такие проблемы, как опечатки, отсутствие свойств и неправильные вызовы функций еще до того, как мы запустим наш код. Типы также используются в редакторах TypeScript, таких как автозавершение, навигация по коду и рефакторинг, которые вы можете увидеть в таких редакторах, как Visual Studio и VS Code. На самом деле, если вы пишете JavaScript в любом из этих редакторов, этот опыт основан на TypeScript! Узнать больше можно на сайте TypeScript.

Вы можете начать использовать TypeScript с помощью npm, выполнив следующую команду:

npm install -D typescript

Запрещенные проверки на Nullish и Truthy

Возможно, вы написали regex и забыли вызвать .test(...) для него:

if (/0x[0-9a-f]/) {
    // Упс! Этот блок всегда выполняется.
    // ...
}

Или, может быть, вы случайно написали => (которая создает стрелочную функцию) вместо >= (оператор "больше-чем-или-равно-то"):

if (x => 0) {
    // Упс! Этот блок всегда выполняется.
    // ...
}

или, возможно, вы пытались использовать значение по умолчанию с помощью ??, но перепутали приоритет ?? и оператора сравнения, например <:

function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
    if (strictness === "loose") {
        value = +value
    }
    return value < options.max ?? 100;
    // Упс! Это будет выполнено как (value < options.max) ?? 100
}

или, может быть, вы неправильно поставили скобку в сложном выражении:

if (
    isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
    isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
    //                           ^^^^ ? Забыли закрывающую скобку ')'?
}

Ни один из этих примеров не делает того, что задумал автор, но все они являются корректным кодом JavaScript. Ранее TypeScript также спокойно принимал эти примеры.

Но, немного поэкспериментировав, мы обнаружили, что многие ошибки можно отловить, если отмечать подозрительные примеры, как описано выше. В TypeScript 5.6 компилятор теперь ошибается, когда синтаксически может определить, что истинностная или нулевая проверка всегда будет оцениваться определенным образом. Поэтому в приведенных выше примерах вы начнете видеть ошибки:

if (/0x[0-9a-f]/) {
//  ~~~~~~~~~~~~
// ошибка: Такое выражение всегда правдиво.
}

if (x => 0) {
//  ~~~~~~
// ошибка: Такое выражение всегда истинно.
}

function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
    if (strictness === "loose") {
        value = +value
    }
    return value < options.max ?? 100;
    //     ~~~~~~~~~~~~~~~~~~~
    // ошибка: Правый операнд ?? недостижим, так как левый операнд никогда не бывает nullish.
}

if (
    isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
    isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
    //                    ~~~~~~~
    // ошибка: Такое выражение всегда истинно.
}

Аналогичных результатов можно добиться, включив правило ESLint no-constant-binary-expression, и вы можете увидеть некоторые из результатов, которых они добились, в их блоге; но новые проверки, которые выполняет TypeScript, не полностью совпадают с правилом ESLint, и мы также считаем, что есть большая польза в том, чтобы эти проверки были встроены в сам TypeScript.

Обратите внимание, что некоторые выражения по-прежнему разрешены, даже если они всегда истинны или равны нулю. В частности, выражения true, false, 0 и 1 все еще разрешены, несмотря на то, что всегда являются истинными или ложными, поскольку код, подобный следующему:

while (true) {
    doStuff();

    if (something()) {
        break;
    }

    doOtherStuff();
}

все еще идиоматичен и полезен, и код, подобный следующему:

if (true || inDebuggingOrDevelopmentEnvironment()) {
    // ...
}

полезна при итерации/отладке кода.

Если вам интересно узнать о реализации или о том, какие ошибки она отлавливает, посмотрите на pull request, в котором реализована эта функция.

Методы-помощники итератора

В JavaScript есть понятие итераторов (вещей, которые мы можем перебирать, вызывая Symbol.iterator и получая итератор) и итераторов (вещей, у которых есть метод next(), который мы можем вызвать, чтобы попытаться получить следующее значение по мере итерации). По большому счету, вам обычно не нужно думать об этих вещах, когда вы бросаете их в цикл for/of или [...распределяете] их в новый массив. Но TypeScript моделирует их с помощью типов Iterable и Iterator (и даже IterableIterator, который действует как оба!), и эти типы описывают минимальный набор членов, необходимых для работы с ними таких конструкций, как for/of.

Итераторы (и IterableIterator) хороши тем, что их можно использовать в самых разных местах в JavaScript - но многие люди обнаружили, что им не хватает таких методов для массивов, как map, filter и, по какой-то причине, reduce. Поэтому недавно в ECMAScript было выдвинуто предложение добавить многие методы (и даже больше) из Array в большинство IterableIterators, которые создаются в JavaScript.

Например, теперь каждый генератор создает объект, который также имеет метод map и метод take.

function* positiveIntegers() {
    let i = 1;
    while (true) {
        yield i;
        i++;
    }
}

const evenNumbers = positiveIntegers().map(x => x * 2);

// Вывод:
//    2
//    4
//    6
//    8
//   10
for (const value of evenNumbers.take(5)) {
    console.log(value);
}

То же самое справедливо и для таких методов, как keys(), values() и entries() на Map и Set.

function invertKeysAndValues<K, V>(map: Map<K, V>): Map<V, K> {
    return new Map(
        map.entries().map(([k, v]) => [v, k])
    );
}

Вы также можете расширить новый объект Iterator:

/**
 * Обеспечивает бесконечный поток `0`.
 */
class Zeroes extends Iterator<number> {
    next() {
        return { value: 0, done: false } as const;
    }
}

const zeroes = new Zeroes();

// Обеспечивает бесконечный поток `1`.
const ones = zeroes.map(x => x + 1);

А с помощью Iterator.from вы можете адаптировать любые существующие Iterables или Iterators к этому новому типу:

Iterator.from(...).filter(someFunction);

Все эти новые методы работают при условии, что вы работаете в более новой среде выполнения JavaScript или используете полифилл для нового объекта Iterator.

Теперь нам нужно поговорить об именовании.

Ранее мы упоминали, что в TypeScript есть типы Iterable и Iterator; однако, как мы уже говорили, они действуют как "протоколы", обеспечивающие работу определенных операций. Это означает, что не каждое значение, объявленное Iterable или Iterator в TypeScript, будет иметь те методы, о которых мы говорили выше.

Но все же существует новое значение времени выполнения, называемое Iterator. Вы можете ссылаться на Iterator, а также на Iterator.prototype как на фактические значения в JavaScript. Это немного неудобно, поскольку в TypeScript уже определена своя собственная вещь под названием Iterator, предназначенная исключительно для проверки типов. Таким образом, из-за этого неудачного столкновения имен TypeScript необходимо ввести отдельный тип для описания этих родных/встроенных итераторов.

В TypeScript 5.6 появился новый тип под названием IteratorObject. Он определяется следующим образом:

interface IteratorObject<T, TReturn = unknown, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    [Symbol.iterator](): IteratorObject<T, TReturn, TNext>;
}

Многие встроенные коллекции и методы создают подтипы IteratorObjects (например, ArrayIterator, SetIterator, MapIterator и другие), и основные типы JavaScript и DOM в lib.d.ts, а также @types/node, были обновлены для использования этого нового типа.

Аналогично, для паритета существует тип AsyncIteratorObject. AsyncIterator пока не существует как значение времени выполнения в JavaScript, которое предоставляет те же методы, что и AsyncIterables, но это активное предложение, и этот новый тип готовится к нему.

Мы хотели бы поблагодарить Кевина Гиббонса, который внес изменения в эти типы и является одним из соавторов предложения.

Строгие проверки встроенного итератора (и --strictBuiltinIteratorReturn)

Когда вы вызываете метод next() для Iterator<T, TReturn>, он возвращает объект со значением и свойством done. Это моделируется с помощью типа IteratorResult.

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

Именование здесь навеяно тем, как работает функция-генератор. Функции-генераторы могут выдавать значения, а затем возвращать конечное значение - но типы между ними могут быть не связаны.

function abc123() {
    yield "a";
    yield "b";
    yield "c";
    return 123;
}

const iter = abc123();

iter.next(); // { value: "a", done: false }
iter.next(); // { value: "b", done: false }
iter.next(); // { value: "c", done: false }
iter.next(); // { value: 123, done: true }

С появлением нового типа IteratorObject мы обнаружили некоторые трудности с обеспечением безопасных реализаций IteratorObject. В то же время, уже давно существовала опасность использования IteratorResult в случаях, когда TReturn был any (по умолчанию!). Например, допустим, у нас есть IteratorResult<string, any>. Если в итоге мы обратимся к значению этого типа, то получим string | any, то есть просто any.

function* uppercase(iter: Iterator<string, any>) {
    while (true) {
        const { value, done } = iter.next();
        yield value.toUppercase(); // Упс! Забыл сначала проверить `done` и неправильно написал `toUpperCase`.

        if (done) {
            return;
        }
    }
}

Сегодня было бы трудно исправить это в каждом итераторе без внесения большого количества изменений, но мы можем, по крайней мере, исправить это в большинстве создаваемых объектов IteratorObject.

TypeScript 5.6 вводит новый внутренний тип под названием BuiltinIteratorReturn и новый флаг --strict-mode под названием --strictBuiltinIteratorReturn. Когда бы ни использовались объекты IteratorObjects в таких местах, как lib.d.ts, они всегда пишутся с типом BuiltinIteratorReturn для TReturn (хотя чаще вы увидите более специфичные MapIterator, ArrayIterator, SetIterator).

interface MapIterator<T> extends IteratorObject<T, BuiltinIteratorReturn, unknown> {
    [Symbol.iterator](): MapIterator<T>;
}

// ...

interface Map<K, V> {
    // ...

    /**
     * Возвращает итерабельную таблицу пар ключ-значение для каждой записи в карте.
     */
    entries(): MapIterator<[K, V]>;

    /**
     * Возвращает итерабельную таблицу ключей в карте.
     */
    keys(): MapIterator<K>;

    /**
     * Возвращает итерируемый список значений в карте.
     */
    values(): MapIterator<V>;
}

По умолчанию BuiltinIteratorReturn равен any, но если включить режим --strictBuiltinIteratorReturn (возможно, через --strict), то он становится неопределенным. В этом новом режиме, если мы используем BuiltinIteratorReturn, наш предыдущий пример теперь корректно ошибается:

function* uppercase(iter: Iterator<string, BuiltinIteratorReturn>) {
    while (true) {
        const { value, done } = iter.next();
        yield value.toUppercase();
        //    ~~~~~ ~~~~~~~~~~~
        // error! ┃      ┃
        //        ┃      ┗━ Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
        //        ┃
        //        ┗━ 'value' is possibly 'undefined'.

        if (done) {
            return;
        }
    }
}

Как правило, в lib.d.ts вы увидите BuiltinIteratorReturn в паре с IteratorObject. В целом, мы рекомендуем быть более явными с TReturn в вашем собственном коде, когда это возможно.

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

Поддержка произвольных идентификаторов модулей

JavaScript позволяет модулям экспортировать привязки с недопустимыми именами идентификаторов в виде строковых литералов:

const banana = "?";

export { banana as "?" };

Аналогично, это позволяет модулям захватывать импорты с такими произвольными именами и привязывать их к корректным идентификаторам:

import { "?" as banana } from "./foo"

/**
 * om nom nom
 */
function eat(food: string) {
    console.log("Eating", food);
};

eat(banana);

Это кажется милым трюком для вечеринки (если вы так же веселы, как мы на вечеринках), но это имеет свое применение для взаимодействия с другими языками (обычно через границы JavaScript/WebAssembly), поскольку в других языках могут быть разные правила для того, что считать правильным идентификатором. Он также может быть полезен для инструментов, генерирующих код, таких как esbuild с его функцией inject.

TypeScript 5.6 теперь позволяет использовать эти произвольные идентификаторы модулей в коде! Мы хотели бы поблагодарить Эвана Уоллеса, который внес это изменение в TypeScript!

Параметр --noUncheckedSideEffectImports

В JavaScript можно импортировать модуль, не импортируя из него никаких значений.

import "some-module";

Такие импорты часто называют импортами побочных эффектов, потому что единственное полезное поведение, которое они могут обеспечить, - это выполнение какого-то побочного эффекта (например, регистрация глобальной переменной или добавление полифилла в прототип).

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

Это удивительное поведение, но оно частично обусловлено шаблонами моделирования в экосистеме JavaScript. Например, этот синтаксис также использовался со специальными загрузчиками в бандлерах для загрузки CSS или других активов. Ваш бандлер может быть настроен таким образом, что вы можете включить определенные .css-файлы, написав что-то вроде следующего:

import "./button-component.css";

export function Button() {
    // ...
}

Тем не менее, это маскирует потенциальные опечатки при импорте побочных эффектов. Поэтому в TypeScript 5.6 появилась новая опция компилятора --noUncheckedSideEffectImports, предназначенная для выявления таких случаев. Когда опция --noUncheckedSideEffectImports включена, TypeScript теперь будет ошибаться, если не сможет найти исходный файл для импорта побочных эффектов.

import "oops-this-module-does-not-exist";
//     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// error: Cannot find module 'oops-this-module-does-not-exist' or its corresponding type declarations.

При включении этой опции некоторый рабочий код может получить ошибку, как в примере с CSS выше. Чтобы обойти эту проблему, пользователям, которые хотят просто написать импорт побочных эффектов для активов, лучше написать так называемое объявление модуля окружения со спецификатором wildcard. Оно будет находиться в глобальном файле и выглядеть примерно следующим образом:

// ./src/globals.d.ts

// Распознает все CSS-файлы как импорт модулей.
declare module "*.css" {}

На самом деле, возможно, у вас уже есть такой файл в вашем проекте! Например, запуск чего-то вроде vite init может создать подобный vite-env.d.ts.

Хотя эта опция сейчас отключена по умолчанию, мы призываем пользователей попробовать!

Для получения дополнительной информации ознакомьтесь с реализацией здесь.

Параметр --noCheck

В TypeScript 5.6 появилась новая опция компилятора, --noCheck, которая позволяет пропустить проверку типов для всех входных файлов. Это позволяет избежать ненужной проверки типов при выполнении любого семантического анализа, необходимого для создания выходных файлов.

Один из сценариев использования этой опции - отделить генерацию JavaScript-файлов от проверки типов, чтобы эти два этапа можно было выполнять отдельно. Например, можно запустить tsc --noCheck во время итерации, а затем tsc --noEmit для тщательной проверки типов. Вы также можете запускать эти две задачи параллельно, даже в режиме --watch, хотя имейте в виду, что вам, вероятно, захочется указать отдельный путь --tsBuildInfoFile, если вы действительно запускаете их одновременно.

Аналогичным образом --noCheck также полезен для эмиссии файлов декларации. В проекте, где указано --noCheck для проекта, соответствующего --isolatedDeclarations, TypeScript может быстро генерировать файлы деклараций без проверки типов. Сгенерированные файлы деклараций будут полагаться исключительно на быстрые синтаксические преобразования.

Обратите внимание, что в случаях, когда указано --noCheck, но проект не использует --isolatedDeclarations, TypeScript все равно может выполнить столько проверок типов, сколько необходимо для генерации файлов .d.ts. В этом смысле --noCheck - немного неправильное название; однако процесс будет более ленивым, чем полная проверка типов, вычисляя только типы неаннотированных деклараций. Это должно быть намного быстрее, чем полная проверка типов.

noCheck также доступен через API TypeScript в качестве стандартной опции. Внутренние модули transpileModule и transpileDeclaration уже использовали noCheck для ускорения работы (по крайней мере, начиная с TypeScript 5.5). Теперь любой инструмент сборки должен уметь использовать этот флаг, применяя различные пользовательские стратегии для координации и ускорения сборки.

Для получения дополнительной информации ознакомьтесь с работой, проделанной в TypeScript 5.5 для внутренней поддержки noCheck, а также с соответствующей работой, чтобы сделать его общедоступным в командной строке

Разрешить --build с промежуточными ошибками

Концепция ссылок на проекты в TypeScript позволяет организовать кодовую базу на несколько проектов и создать зависимости между ними. Запуск компилятора TypeScript в режиме --build (или сокращенно tsc -b) - это встроенный способ провести сборку между проектами и выяснить, какие проекты и файлы должны быть скомпилированы.

Ранее при использовании режима --build предполагалось --noEmitOnError и немедленная остановка сборки при возникновении ошибок. Это означало, что "нижележащие" проекты никогда не могли быть проверены и собраны, если какие-либо из их "вышележащих" зависимостей имели ошибки сборки. Теоретически, такой подход очень удобен - если в проекте есть ошибки, то он не обязательно находится в согласованном состоянии для своих зависимостей.

В реальности же подобная жесткость превращала такие вещи, как обновление, в мучение. Например, если проектВ зависит от проектаА, то люди, знакомые с проектомВ, не могут проактивно обновлять свой код, пока не обновятся их зависимости. Они блокируются работой по обновлению проектаА.

Начиная с TypeScript 5.6, режим --build будет продолжать собирать проекты, даже если в зависимостях есть промежуточные ошибки. При возникновении промежуточных ошибок они будут сообщаться последовательно, а выходные файлы будут генерироваться по мере сил; однако сборка будет продолжаться до завершения на указанном проекте.

Если вы хотите остановить сборку на первом проекте с ошибками, вы можете использовать новый флаг --stopOnBuildErrors. Это может быть полезно при работе в среде CI или при итерациях над проектом, от которого сильно зависят другие проекты.

Обратите внимание, что для достижения этой цели TypeScript теперь всегда создает файл .tsbuildinfo для любого проекта при вызове --build (даже если не указано --incremental/--composite). Это необходимо для того, чтобы отслеживать состояние того, как была вызвана --build, и какие работы необходимо выполнить в будущем.

Подробнее об этом изменении вы можете прочитать здесь, в разделе о реализации.

Приоритетная региональная диагностика в редакторах

Когда языковой сервис TypeScript запрашивает диагностику файла (такие вещи, как ошибки, предложения и устаревания), обычно требуется проверить весь файл. В большинстве случаев это нормально, но в очень больших файлах это может привести к задержке. Это может расстраивать, поскольку исправление опечатки должно быть быстрой операцией, но в достаточно большом файле на это могут уйти секунды.

Чтобы решить эту проблему, в TypeScript 5.6 появилась новая возможность, называемая диагностикой с приоритетом региона или проверкой с приоритетом региона. Вместо того чтобы просто запрашивать диагностику для набора файлов, редакторы теперь могут также предоставить соответствующую область данного файла - причем предполагается, что это будет, как правило, та область файла, которая в данный момент видна пользователю. Сервер языка TypeScript может выбрать, какой из двух наборов диагностических данных предоставить: один для региона, а другой - для файла целиком. Это позволяет сделать редактирование более отзывчивым в больших файлах, и вы не будете так долго ждать, пока исчезнут красные загогулины.

Если говорить о конкретных цифрах, то в нашем тестировании на собственном файле TypeScript checker.ts полный ответ на семантическую диагностику занял 3330мс. В отличие от этого, ответ на первый диагностический ответ на основе региона занял 143мс! В то время как оставшийся ответ на весь файл занял около 3200мс, это может иметь огромное значение для быстрых правок.

Эта функция также включает в себя довольно много работы, чтобы сделать диагностические сообщения более последовательными на протяжении всей работы. Из-за того, как наша программа проверки типов использует кэширование, чтобы избежать работы, последующие проверки одних и тех же типов часто могли иметь другое (обычно более короткое) сообщение об ошибке. Технически, ленивая внепорядковая проверка может привести к тому, что диагностика будет выдавать разные сообщения в двух местах редактора - даже до появления этой функции - но мы не хотели усугублять эту проблему. В результате недавней работы мы устранили многие из этих несоответствий ошибок.

В настоящее время эта функция доступна в Visual Studio Code для TypeScript 5.6 и более поздних версий.

Для получения более подробной информации ознакомьтесь с реализацией и описанием здесь.

Подробные символы фиксации

Языковая служба TypeScript теперь предоставляет собственные символы фиксации для каждого элемента завершения. Символы фиксации - это определенные символы, которые при наборе автоматически фиксируют предложенный в данный момент элемент завершения.

Это означает, что со временем ваш редактор будет чаще фиксировать предложенный в данный момент элемент завершения при вводе определенных символов. Например, возьмем следующий код:

declare let food: {
    eat(): any;
}

let f = (foo/**/

Если наш курсор находится в точке /**/, то непонятно, будет ли код, который мы пишем, чем-то вроде let f = (food.eat()) или let f = (foo, bar) => foo + bar. Можно представить, что редактор может автозаполнять код по-разному в зависимости от того, какой символ мы вводим следующим. Например, если мы вводим символ точки (.), мы, вероятно, хотим, чтобы редактор завершил ввод переменной food; но если мы вводим символ запятой (,), мы можем записать параметр в стрелочной функции.

К сожалению, раньше TypeScript просто сигнализировал редакторам, что текущий текст может определять новое имя параметра, так что никакие символы фиксации не были безопасны. Поэтому нажатие на . ничего не давало, даже если было "очевидно", что редактор должен автозаполнить слово food.

Теперь TypeScript явно указывает, какие символы безопасны для фиксации для каждого элемента завершения. Хотя это не сразу изменит ваш повседневный опыт, редакторы, поддерживающие эти символы фиксации, со временем должны увидеть улучшения в поведении. Чтобы увидеть эти улучшения прямо сейчас, вы можете использовать расширение TypeScript nightly в Visual Studio Code Insiders. Нажатие . в приведенном выше коде корректно автозаполняется food.

Для получения дополнительной информации см. запрос на исправление, в котором были добавлены символы фиксации, а также наши корректировки символов фиксации в зависимости от контекста.

Исключение шаблонов для автоматического импорта

Языковая служба TypeScript теперь позволяет указать список шаблонов регулярных выражений, которые будут отсеивать предложения автоимпорта из определенных спецификаторов. Например, если вы хотите исключить весь "глубокий" импорт из такого пакета, как lodash, вы можете настроить следующее предпочтение в Visual Studio Code:

{
    "typescript.preferences.autoImportSpecifierExcludeRegexes": [
        "^lodash/.*$"
    ]
}

Или, наоборот, вы можете запретить импорт из точки входа в пакет:

{
    "typescript.preferences.autoImportSpecifierExcludeRegexes": [
        "^lodash$"
    ]
}

Можно даже избежать импорта node:, используя следующую настройку:

{
    "typescript.preferences.autoImportSpecifierExcludeRegexes": [
        "^node:"
    ]
}

Чтобы указать некоторые флаги регулярных выражений, такие как i или u, вам нужно окружить регулярное выражение косыми ковычками. При указании окружающих косых ковычек, вам нужно будет исключить другие внутренние косые ковычки.

{
    "typescript.preferences.autoImportSpecifierExcludeRegexes": [
        "^./lib/internal",        // no escaping needed
        "/^.\\/lib\\/internal/",  // escaping needed - note the leading and trailing slashes
        "/^.\\/lib\\/internal/i"  // escaping needed - we needed slashes to provide the 'i' regex flag
    ]
}

Те же настройки можно применить для JavaScript через javascript.preferences.autoImportSpecifierExcludeRegexes в VS Code.

Обратите внимание, что хотя эта опция может немного пересекаться с typescript.preferences.autoImportFileExcludePatterns, есть и различия. Существующий autoImportFileExcludePatterns принимает список шаблонов glob, которые исключают пути к файлам. Это может быть проще для многих сценариев, когда вы хотите избежать автоимпорта из определенных файлов и каталогов, но этого не всегда достаточно. Например, если вы используете пакет @types/node, в одном и том же файле объявлены и fs, и node:fs, поэтому мы не можем использовать autoImportExcludePatterns для фильтрации того или другого.

Новая функция autoImportSpecifierExcludeRegexes предназначена для спецификаторов модулей (конкретной строки, которую мы пишем в операторе импорта), поэтому мы можем написать шаблон для исключения fs или node:fs, не исключая другого. Более того, мы можем написать паттерны, которые заставят автоимпорт предпочесть различные стили спецификаторов (например, предпочесть ./foo/bar.js вместо #foo/bar.js).

Для получения дополнительной информации смотрите реализацию здесь.

Заметные изменения в поведении

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

lib.d.ts

Типы, генерируемые для DOM, могут повлиять на проверку типов в вашей кодовой базе. Для получения дополнительной информации смотрите связанные проблемы, связанные с DOM, и обновления lib.d.ts для этой версии TypeScript.

.tsbuildinfo записывается всегда

Чтобы --build мог продолжать сборку проектов даже при наличии промежуточных ошибок в зависимостях, а также для поддержки --noCheck в командной строке, TypeScript теперь всегда создает файл .tsbuildinfo для любого проекта при вызове --build. Это происходит независимо от того, включен ли параметр --incremental. Дополнительную информацию можно найти здесь.

Соблюдение расширений файлов и package.json внутри node_modules

До того как Node.js реализовал поддержку модулей ECMAScript в версии 12, у TypeScript не было хорошего способа узнать, представляют ли файлы .d.ts, найденные в node_modules, файлы JavaScript, созданные как модули CommonJS или ECMAScript. Когда подавляющее большинство npm было только CommonJS, это не вызывало особых проблем - в случае сомнений TypeScript мог просто предположить, что все ведет себя как CommonJS. К сожалению, если такое предположение было неверным, это могло привести к небезопасному импорту:

// node_modules/dep/index.d.ts
export declare function doSomething(): void;

// index.ts
// Все хорошо, если "dep" - модуль CommonJS, но не работает, если
// это модуль ECMAScript - даже в бандлерах!
import dep from "dep";
dep.doSomething();

На практике это случалось нечасто. Но за годы, прошедшие с тех пор, как Node.js начал поддерживать модули ECMAScript, доля ESM на npm выросла. К счастью, в Node.js также появился механизм, который может помочь TypeScript определить, является ли файл модулем ECMAScript или модулем CommonJS: расширения файлов .mjs и .cjs и поле package.json "type". В TypeScript 4.7 была добавлена поддержка понимания этих индикаторов, а также авторинга файлов .mts и .cts; однако TypeScript считывал эти индикаторы только при использовании --module node16 и --module nodenext, поэтому вышеописанный небезопасный импорт по-прежнему был проблемой для тех, кто использовал --module esnext и --moduleResolution bundler, например.

Чтобы решить эту проблему, TypeScript 5.6 собирает информацию о формате модуля и использует ее для разрешения двусмысленностей, подобных той, что описана в примере выше, во всех модульных режимах (кроме amd, umd и system). Расширения файлов, специфичные для формата (.mts и .cts), соблюдаются везде, где они встречаются, а поле package.json "type" используется в зависимостях node_modules независимо от настроек модуля. Ранее существовала техническая возможность создания вывода CommonJS в файл .mjs и наоборот:

// main.mts
export default "oops";

// $ tsc --module commonjs main.mts
// main.mjs
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = "oops";

Теперь файлы .mts никогда не будут выводить CommonJS, а файлы .cts - ESM.

Обратите внимание, что многое из этого поведения было предусмотрено в предрелизных версиях TypeScript 5.5 (подробности реализации здесь), но в 5.6 это поведение распространяется только на файлы внутри node_modules.

Более подробная информация об этом изменении доступна здесь.

Корректное переопределение проверок вычисленных свойств

Ранее вычисляемые свойства, помеченные значком override, некорректно проверяли существование члена базового класса. Аналогично, если вы использовали noImplicitOverride, вы не получали ошибку, если забывали добавить модификатор override к вычисляемому свойству.

Теперь TypeScript 5.6 корректно проверяет вычисляемые свойства в обоих случаях.

const foo = Symbol("foo");
const bar = Symbol("bar");

class Base {
    [bar]() {}
}

class Derived extends Base {
    override [foo]() {}
//           ~~~~~
// error: This member cannot have an 'override' modifier because it is not declared in the base class 'Base'.

    [bar]() {}
//  ~~~~~
// error under noImplicitOverride: This member must have an 'override' modifier because it overrides a member in the base class 'Base'.
}

Это исправление было внесено благодаря Александру Тарасюку в этом запросе на исправление.

Что дальше?

Если вы хотите узнать, что будет дальше, вы также можете взглянуть на план итераций TypeScript 5.7, где вы увидите список приоритетных функций, исправлений ошибок и целевых дат выпуска, которые вы можете планировать. Релизы TypeScript nightly легко использовать через npm, а также есть расширение для использования этих релизов в Visual Studio Code. Эти релизы, как правило, не мешают, но они могут дать вам хорошее представление о том, что будет дальше, а также помогают проекту TypeScript отлавливать ошибки на ранней стадии!

В остальном мы надеемся, что TypeScript 5.6 подарит вам отличные впечатления и сделает ваш ежедневный кодинг радостным!

Счастливого хакинга! Даниэль Розенвассер и команда TypeScript

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