Эта статья — перевод оригинальной статьи «Announcing TypeScript 5.7»
Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Сегодня мы рады сообщить о выходе TypeScript 5.7!
Если вы не знакомы с TypeScript, то это язык, который развивает JavaScript, добавляя синтаксис для деклараций типов и аннотаций. Этот синтаксис может быть использован компилятором TypeScript для проверки типов нашего кода, а также может быть удален для создания чистого, идиоматического JavaScript-кода. Проверка типов полезна тем, что позволяет заранее выявить ошибки в нашем коде, но добавление типов в код также делает его более читаемым и позволяет таким инструментам, как редакторы кода, предоставлять нам такие мощные возможности, как автозавершение, рефакторинг, поиск всех ссылок и многое другое. TypeScript является надмножеством JavaScript, поэтому любой правильный код JavaScript также является правильным кодом TypeScript, и если вы пишете JavaScript в редакторе, таком как Visual Studio или VS Code, TypeScript также является основой вашего опыта работы с редактором JavaScript! Вы можете узнать больше о TypeScript на нашем сайте typescriptlang.org.
Чтобы начать использовать TypeScript, выполните следующую команду через npm:
npm install -D typescript
Давайте посмотрим, что нового появилось в TypeScript 5.7!
Проверки для никогда не инициализируемых переменных
TypeScript уже давно умеет отлавливать проблемы, когда переменная еще не была инициализирована во всех предыдущих ветках.
let result: number if (someCondition()) { result = doSomeWork(); } else { let temporaryWork = doSomeWork(); temporaryWork *= 2; // забыли присвоить 'result' } console.log(result); // error: Переменная 'result' используется до присвоения.
К сожалению, есть места, где этот анализ не работает. Например, если доступ к переменной осуществляется в отдельной функции, система типов не знает, когда будет вызвана функция, и вместо этого принимает «оптимистическое» мнение, что переменная будет инициализирована.
function foo() { let result: number if (someCondition()) { result = doSomeWork(); } else { let temporaryWork = doSomeWork(); temporaryWork *= 2; // забыли присвоить 'result' } printResult(); function printResult() { console.log(result); // ошибки нет. } }
Хотя TypeScript 5.7 по-прежнему снисходителен к переменным, которые, возможно, были инициализированы, система типов способна сообщать об ошибках, когда переменные вообще не были инициализированы.
function foo() { let result: number // работает, но забыл присвоить "результат". function printResult() { console.log(result); // error: Переменная 'result' используется до присвоения. } }
Это изменение было внесено благодаря работе пользователя GitHub Zzzen!
Переписывание путей для относительных маршрутов
Существует несколько инструментов и режимов выполнения, которые позволяют запускать код TypeScript «на месте», то есть они не требуют шага сборки, который генерирует выходные файлы JavaScript. Например, ts-node, tsx, Deno и Bun поддерживают запуск .ts
файлов напрямую. Совсем недавно Node.js исследовал такую поддержку с помощью --experimental-strip-types
(скоро будет снят флаг!) и --experimental-transform-types
. Это очень удобно, поскольку позволяет нам быстрее выполнять итерации, не беспокоясь о повторном запуске задачи сборки.
Однако при использовании этих режимов необходимо помнить о некоторых сложностях. Чтобы обеспечить максимальную совместимость со всеми этими инструментами, файл TypeScript, импортированный «in-place», должен быть импортирован с соответствующим расширением TypeScript во время выполнения. Например, чтобы импортировать файл foo.ts
, мы должны написать следующее в новой экспериментальной поддержке Node:
// main.ts import * as foo from "./foo.ts"; // <- Здесь нужен foo.ts, а не foo.js
Обычно TypeScript выдает ошибку, если мы делаем это, поскольку ожидает, что мы импортируем выходной файл. Так как некоторые инструменты позволяют импортировать .ts
, TypeScript уже некоторое время поддерживает этот стиль импорта с помощью опции --allowImportingTsExtensions
. Это прекрасно работает, но что произойдет, если нам понадобится сгенерировать .js-файлы из этих .ts-файлов? Это требование для авторов библиотек, которым нужно иметь возможность распространять только .js-файлы, но до сих пор TypeScript избегал переписывания путей.
Чтобы поддержать этот сценарий, мы добавили новую опцию компилятора --rewriteRelativeImportExtensions
. Если путь импорта является относительным (начинается с ./ или ../), заканчивается расширением TypeScript (.ts, .tsx, .mts, .cts) и является файлом без декларации, компилятор перепишет путь в соответствующее расширение JavaScript (.js, .jsx, .mjs, .cjs).
// Вместе с --rewriteRelativeImportExtensions... // они будут переписаны. import * as foo from "./foo.ts"; import * as bar from "../someFolder/bar.mts"; // они НИ В КОЕМ случае не будут переписаны. import * as a from "./foo"; import * as b from "some-package/file.ts"; import * as c from "@some-scope/some-package/file.ts"; import * as d from "#/file.ts"; import * as e from "./file.js";
Это позволяет нам писать код TypeScript, который можно запускать на месте, а затем компилировать в JavaScript, когда мы будем готовы.
Итак, мы отметили, что TypeScript обычно избегает переписывания путей. На это есть несколько причин, но самая очевидная из них — динамический импорт. Если разработчик напишет следующее, то будет нетривиально обработать путь, который получит «import
«. Фактически, невозможно переопределить поведение «import
«в любых зависимостях.
function getPath() { if (Math.random() < 0.5) { return "./foo.ts"; } else { return "./foo.js"; } } let myImport = await import(getPath());
Другая проблема заключается в том, что (как мы видели выше) переписываются только относительные пути, а они написаны «наивно». Это означает, что любой путь, который опирается на baseUrl
и paths
в TypeScript, не будет переписан:
// tsconfig.json { "compilerOptions": { "module": "nodenext", // ... "paths": { "@/*": ["./src/*"] } } }
// Не преобразуется, не работает. import * as utilities from "@/utilities.ts";
Также как и любой путь, который может быть разрешен через поля exports и imports в package.json
.
// package.json { "name": "my-package", "imports": { "#root/*": "./dist/*" } }
// Не преобразуется, не работает. import * as utilities from "#root/utilities.ts";
В результате, если вы используете схему с несколькими пакетами, ссылающимися друг на друга, вам может понадобиться использовать условный экспорт с настраиваемыми условиями, чтобы это работало:
// my-package/package.json { "name": "my-package", "exports": { ".": { "@my-package/development": "./src/index.ts", "import": "./lib/index.js" }, "./*": { "@my-package/development": "./src/*.ts", "import": "./lib/*.js" } } }
В любой момент, когда вы захотите импортировать файлы .ts
, вы можете запустить его с помощью node --conditions=@my-package/development
.
Обратите внимание на «пространство имен» или «область действия», которое мы использовали для условия @my-package/development
. Это немного временное решение, чтобы избежать конфликтов с зависимостями, которые также могут использовать условие development
. Если все поставляют development
в своем пакете, то разрешение может попытаться разрешиться в .ts-файл, что не всегда сработает. Эта идея похожа на то, что описано в эссе Колина Макдоннелла Live types in a TypeScript monorepo, а также в руководстве tshy по загрузке из исходников.
Более подробно о том, как работает эта функция, читайте здесь.
Поддержка —target es2024 и—lib es2024
TypeScript 5.7 теперь поддерживает --target es2024
, которая позволяет пользователям нацеливаться на ECMAScript 2024 runtimes. Эта цель в первую очередь позволяет указать новую --lib es2024
, которая содержит множество функций для SharedArrayBuffer
и ArrayBuffer
, Object.groupBy
, Map.groupBy
, Promise.withResolvers
и многое другое. Он также переносит Atomics.waitAsync
из --lib es2022
в --lib es2024
.
Обратите внимание, что в результате изменений SharedArrayBuffer
и ArrayBuffer
теперь немного расходятся. Чтобы устранить разрыв и сохранить базовый тип буфера, все TypedArray
(например, Uint8Array
и другие) теперь также являются Generic.
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> { // ... }
Каждый TypedArray
теперь содержит параметр типа с именем TArrayBuffer
, хотя этот параметр типа имеет аргумент типа по умолчанию, так что мы можем продолжать ссылаться на Int32Array
без явной записи Int32Array<ArrayBufferLike>
.
Если вы столкнулись с какими-либо проблемами в рамках этого обновления, вам может потребоваться обновить @types/node
.
Эта работа была выполнена в основном благодаря Кента Мориучи!
Поиск файлов конфигурации предков на предмет владения проектом
Когда файл TypeScript загружается в редактор, использующий TSServer (например, Visual Studio или VS Code), редактор попытается найти соответствующий файл tsconfig.json
, который «владеет» этим файлом. Для этого он поднимается вверх по дереву каталогов от редактируемого файла и ищет любой файл с именем tsconfig.json
.
Ранее этот поиск останавливался на первом найденном файле tsconfig.json
; однако представьте себе следующую структуру проекта:
project/ ├── src/ │ ├── foo.ts │ ├── foo-test.ts │ ├── tsconfig.json │ └── tsconfig.test.json └── tsconfig.json
Идея заключается в том, что src/tsconfig.json
— это «основной» файл конфигурации проекта, а src/tsconfig.test.json
— файл конфигурации для запуска тестов.
// src/tsconfig.json { "compilerOptions": { "outDir": "../dist" }, "exclude": ["**/*.test.ts"] }
// src/tsconfig.test.json { "compilerOptions": { "outDir": "../dist/test" }, "include": ["**/*.test.ts"], "references": [ { "path": "./tsconfig.json" } ] }
// tsconfig.json { // Вместо того чтобы указывать какие-либо файлы, он просто ссылается на все реальные проекты. "files": [], "references": [ { "path": "./src/tsconfig.json" }, { "path": "./src/tsconfig.test.json" }, ] }
Проблема здесь в том, что при редактировании foo-test.ts
редактор обнаружит project/src/tsconfig.json
в качестве «собственного» файла конфигурации — но это не тот файл, который нам нужен! Если поиск остановится на этом моменте, это может оказаться нежелательным. Единственным способом избежать этого ранее было переименование src/tsconfig.json
во что-то вроде src/tsconfig.src.json
, и тогда все файлы попадали бы в верхний уровень tsconfig.json
, который ссылается на все возможные проекты.
project/ ├── src/ │ ├── foo.ts │ ├── foo-test.ts │ ├── tsconfig.src.json │ └── tsconfig.test.json └── tsconfig.json
Вместо того чтобы заставлять разработчиков делать это, TypeScript 5.7 теперь продолжает идти вверх по дереву каталогов, чтобы найти другие подходящие файлы tsconfig.json
для сценариев редактора. Это может обеспечить большую гибкость в организации проектов и структурировании конфигурационных файлов.
Более подробную информацию о реализации можно получить на GitHub здесь и здесь.
Ускоренная проверка владения проектом в редакторах для composite проектов
Представьте себе большую кодовую базу со следующей структурой:
packages ├── graphics/ │ ├── tsconfig.json │ └── src/ │ └── ... ├── sound/ │ ├── tsconfig.json │ └── src/ │ └── ... ├── networking/ │ ├── tsconfig.json │ └── src/ │ └── ... ├── input/ │ ├── tsconfig.json │ └── src/ │ └── ... └── app/ ├── tsconfig.json ├── some-script.js └── src/ └── ...
Каждая директория в packages
— это отдельный TypeScript-проект, а директория app
— это основной проект, который зависит от всех остальных проектов.
// app/tsconfig.json { "compilerOptions": { // ... }, "include": ["src"], "references": [ { "path": "../graphics/tsconfig.json" }, { "path": "../sound/tsconfig.json" }, { "path": "../networking/tsconfig.json" }, { "path": "../input/tsconfig.json" } ] }
Теперь обратите внимание, что в каталоге app
у нас есть файл some-script.js
. Когда мы открываем some-script.js
в редакторе, служба языка TypeScript (которая также управляет работой редактора с файлами JavaScript!) должна выяснить, к какому проекту принадлежит файл, чтобы применить нужные настройки.
В этом случае ближайший tsconfig.json
не содержит some-script.js
, но TypeScript продолжит спрашивать: «Может ли один из проектов, на которые ссылается app/tsconfig.json
, содержать some-script.js
?». Для этого TypeScript ранее загружал каждый проект по очереди и останавливался, как только находил проект, содержащий some-script.js
. Даже если some-script.js
не включен в корневой набор файлов, TypeScript все равно будет разбирать все файлы в проекте, потому что некоторые из корневого набора файлов все равно могут транзитивно ссылаться на some-script.js
.
Со временем мы обнаружили, что такое поведение приводит к экстремальному и непредсказуемому поведению в больших кодовых базах. Разработчики открывали нестандартные файлы сценариев и оказывались в ожидании, пока откроется вся их кодовая база.
К счастью, каждый проект, на который может ссылаться другой (не рабочий) проект, должен включать флаг, называемый composite
, который обеспечивает соблюдение правила, согласно которому все исходные файлы должны быть известны заранее. Поэтому при проверке composite
проекта TypeScript 5.7 будет проверять только принадлежность файла к корневому набору файлов этого проекта. Это позволит избежать такого распространенного поведения в худшем случае.
Более подробную информацию об изменениях можно найти здесь.
Проверенный импорт JSON в —module nodenext
При импорте из .json-файла по команде --module nodenext
, TypeScript теперь будет применять определенные правила для предотвращения ошибок во время выполнения.
Например, атрибут импорта, содержащий type: "json"
должен присутствовать для любого импорта JSON-файла.
import myConfig from "./myConfig.json"; // ~~~~~~~~~~~~~~~~~ // ❌ Ошибка: Импорт JSON-файла в модуль ECMAScript требует 'type: "json"' атрибут импорта, если для 'module' установлено значение 'NodeNext'. import myConfig from "./myConfig.json" with { type: "json" }; // ^^^^^^^^^^^^^^^^ // ✅ Это нормально, потому что мы предоставили `type: "json"`.
В дополнение к этой проверке TypeScript не будет генерировать «именованные» экспорты, а содержимое JSON-импорта будет доступно только по умолчанию.
// ✅ Это нормально: import myConfigA from "./myConfig.json" with { type: "json" }; let version = myConfigA.version; /////////// import * as myConfigB from "./myConfig.json" with { type: "json" }; // ❌ Это нет: let version = myConfig.version; // ✅ Это нормально: let version = myConfig.default.version;
Более подробную информацию об этом изменении смотрите здесь.
Поддержка кэширования компиляции V8 в Node.js
Node.js 22 поддерживает новый API под названием module.enableCompileCache(). Этот API позволяет среде выполнения повторно использовать часть работы по разбору и компиляции, выполненной после первого запуска инструмента.
TypeScript 5.7 теперь использует этот API, чтобы быстрее начать выполнять полезную работу. В ходе собственных тестов мы убедились, что выполнение команды tsc --version
ускоряет работу в 2,5 раза.
Benchmark 1: node ./built/local/_tsc.js --version (*without* caching) Time (mean ± σ): 122.2 ms ± 1.5 ms [User: 101.7 ms, System: 13.0 ms] Range (min … max): 119.3 ms … 132.3 ms 200 runs Benchmark 2: node ./built/local/tsc.js --version (*with* caching) Time (mean ± σ): 48.4 ms ± 1.0 ms [User: 34.0 ms, System: 11.1 ms] Range (min … max): 45.7 ms … 52.8 ms 200 runs Summary node ./built/local/tsc.js --version ran 2.52 ± 0.06 times faster than node ./built/local/_tsc.js --version
Дополнительную информацию можно найти в пулл-реквесте здесь.
Заметные изменения в поведении
В этом разделе выделяется набор заслуживающих внимания изменений, которые следует признать и понять при любом обновлении. Иногда в нем выделяются обесценивания, удаления и новые ограничения. Он также может содержать исправления ошибок, которые являются функциональными улучшениями, но которые также могут повлиять на существующую сборку, внеся новые ошибки.
lib.d.ts
Типы, генерируемые для DOM, могут повлиять на проверку типов в вашей кодовой базе. Для получения дополнительной информации смотрите связанные проблемы, связанные с DOM, и обновления lib.d.ts для этой версии TypeScript.
TypedArrays теперь Generic над ArrayBufferLike
В ECMAScript 2024 типы SharedArrayBuffer
и ArrayBuffer
немного расходятся. Чтобы устранить разрыв и сохранить базовый тип буфера, все TypedArray
(например, Uint8Array
и другие) теперь также являются generic.
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> { // ... }
Каждый TypedArray
теперь содержит параметр типа с именем TArrayBuffer
, хотя этот параметр типа имеет аргумент типа по умолчанию, так что пользователи могут продолжать ссылаться на Int32Array
без явной записи Int32Array<ArrayBufferLike>
.
Если вы столкнулись с какими-либо проблемами, связанными с этим обновлением, например
error TS2322: Type 'Buffer' is not assignable to type 'Uint8Array<ArrayBufferLike>'. error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'Uint8Array<ArrayBufferLike>'. error TS2345: Argument of type 'ArrayBufferLike' is not assignable to parameter of type 'ArrayBuffer'. error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'string | ArrayBufferView | Stream | Iterable<string | ArrayBufferView> | AsyncIterable<string | ArrayBufferView>'.
то вам, возможно, потребуется обновить @types/node
.
Подробную информацию об этом изменении вы можете прочитать на GitHub.
Создание индексных подписей из нелитеральных имен методов в классах
TypeScript теперь более последовательно ведет себя с методами в классах, когда они объявляются с нелитеральными вычисляемыми именами свойств. Например, в следующем примере:
declare const symbolMethodName: symbol; export class A { [symbolMethodName]() { return 1 }; }
Раньше TypeScript просто рассматривал класс следующим образом:
export class A { }
Другими словами, с точки зрения системы типов, [symbolMethodName]
ничего не вносит в тип A.
Теперь TypeScript 5.7 рассматривает метод [symbolMethodName {}]
более осмысленно и генерирует индексную сигнатуру. В результате приведенный выше код интерпретируется как нечто похожее на следующий код:
export class A { [x: symbol]: () => number; }
Это обеспечивает поведение, соответствующее свойствам и методам в объектных литералах.
Подробнее об этом изменении читайте здесь.
Больше неявных ошибок на функциях, возвращающих null и undefined
Когда выражение функции контекстно типизировано сигнатурой, возвращающей общий тип, TypeScript теперь корректно предоставляет неявную ошибку any
в рамках noImplicitAny
, но за пределами strictNullChecks
.
declare var p: Promise<number>; const p2 = p.catch(() => null); // ~~~~~~~~~~ // error TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.
Более подробную информацию см. в этом изменении.
Что дальше?
В ближайшее время мы расскажем о наших планах на следующий выпуск TypeScript, но если вам нужны самые свежие исправления и функции, мы упрощаем использование ночных сборок TypeScript на npm, а также публикуем расширение для использования этих ночных сборок в Visual Studio Code.
В остальном мы надеемся, что TypeScript 5.7 сделает кодинг для вас удовольствием. Счастливого хакинга!
Даниэль Розенвассер и команда TypeScript
ссылка на оригинал статьи https://habr.com/ru/articles/861126/
Добавить комментарий