beta

4.5

Поддержка ECMAScript модулей в NodeJs

Начиная с текущей версии TypeScript реализует поддержку ECMAScript (ESM) модулей в среде nodejs. Для активации данного функционала, в tsconfig.json необходимо задать свойству module значение nodenext.

json
{ "compilerOptions": { "module": "nodenext", } }

Основные правила придерживаются спецификации ECMAScript, но существует один момент, о котором стоит упомянуть.

В случае активации esm в package.json и выборе модулей nodenext относительные пути в импортах требуют уточнение расширения, как .js. Другими словами импорт файла file.(ts|tsx|js|jsx) должен выглядить, как import "./file.js", а не import "./file".

ts
// file f.ts export const f = () => {}; // file index.ts import {f} from "./f"; // Error import {f} from "./f.js"; // Ok

Как известно, при реализации esm в nodejs появилась поддержка двух новых форматов файлов - .mjs для ECMAScript модулей и .cjs для CommonJs модулей. Следуя этому, в TypeScript также появилась поддержка двух новых расширений .mts и .cts, и кроме того .d.mts и .d.cts.

ECMAScript позволяют импортировать CommonJs модули таким образом, как если бы они имели экспор по умолчанию.

ts
// file f.сts export const f = () => {}; // file index.ьts import f from "./f";

Кроме этого, после реализации esm nodejs, в package.json было добавлено новое свойство exports позволяющее конкретизировать точки входа.

json
// package.json // package.json { "type": "module", "exports": { ".": { "import": "./esm/index.js", "require": "./commonjs/index.cjs", }, }, // для поддержки старых версий "main": "./commonjs/index.cjs", }

Таким образом, TypeScript также добавляет новую возможность указания деклараций типов.

json
// package.json { "type": "module", "exports": { ".": { "import": "./esm/index.js", "require": "./commonjs/index.cjs", "types": "./types/index.d.ts" }, }, // для поддержки старых версий "main": "./commonjs/index.cjs", "types": "./types/index.d.ts" }

Поддержка lib из node_modules

Вместе с изменениями самого языка_TypeScript_ изменяеются и его библиотеки описывающие api JavaScript. Это является основной проблемой при переходе на новую версию, что чаще всего особенно заметно в контексте DOM API. Поэтому, неачиная с текущей версии TypeScript позволяет устанавливать конкретные версии библиотек входящих в групперовку lib. Точно также, как компилятор начинает поиск обычных декларации с директории node_modules/@types, поиск библиотечных деклараций будет осуществлятся с директории node_modules/#typescript/lib-*.

Таким образом разработчики самостоятельно смогут устанавливать конкретные версии библиотек при помощи @types менеджера.

bash
> npm i -D @types/web
json
// package.json { "devDependencies": { "@typescript/lib-dom": "npm:@types/web" } }

Тип Awaited

Новый расширенный тип Awaited<T> предназначен для рекурсивного развертывания промисов, поможет без труда указывать тип, к которому принадлежит результат асинхронных операций.

ts
// A = string type A = Awaited<Promise<string>>; // B = string type B = Awaited<Promise<Promise<string>>>; // C = string | number type C = Awaited<string | Promise<number>>;

Указание шаблонного литерального строкового типа в качестве дискриминанта

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

ts
interface Success { type: `${string}Success`; body: string; } interface Error { type: `${string}Error`; message: string; } function handler(result: Success | Error) { if (result.type === "HttpSuccess") { let token = result.body; } }

Стабильная поддержка --module es2022

Начиная с текущей версии TypeScript реализует стабильную поддержку модулей es2022, основная цель которых указания ключевого слова await вне тела функции помеченной, как async. Данное поведение активируется при указании свойсту module значений esnext или nodenext для nodejs.

Исключение хвостовой рекурсии на условных типа

Безусловно, рекурсивные типы значительно упращают написание кода, но из-за ограничений связанных с бесконечным циклом не могут показать себя полность. Представьте, что необходимо создать тип, который будет удалять крайние пробелы у литерального строкогого типа.

ts
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; type Test = TrimLeft<" text">; // type Test = "text"

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

ts
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; /** * До текущей версии [*] ошибка -> * Type instantiation is excessively deep and possibly infinite.ts(2589) */ type Test = TrimLeft<" text">; // [*]

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

ts
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; /** * Начиная с текущей версии [*] Ok! */ type Test = TrimLeft<" text">; // [*]

Отключение исключения неиспользуемого импорта

Не так давно, в TypeScript был реализован механизм предназначеный для исключения импортов для неиспользуемых констркций, но затем выяснилось, что в некоторых случаях, это поведение является нежелатильным. К примеру, известный фраймворк vuejs выполняет импорт конструкций в теле тегов <script></script>, а использует их в шаблоне описывающем структуру компонентов.

ts
<script setup> import { handler } from "./handlers.ts"; </script> <button @click="handler">Нажми меня!</button>

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

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

Совмещение import type с обычным import

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

ts
// constructions.ts export class A { a = ``; } export class B { b = 5; } // index.ts import {A} from "./components.ts"; // импорт класса CustomComponet из файла components.ts // импорт только типа Componet из того же файла import type {B} from "./components.ts";

Начиная с текущей версии импорт конструкции и импорт только типов можно осуществлять в одной операции.

ts
import {type A, B} from "./constructions.ts";

К тому же, подобный импорт можно совмещать с импортом по умолчанию, но при условии, что будет импортированна конструкция, а не тип.

ts
// constructions.ts export class A { a = ``; } export class B { b = 5; } export default class C { c = true; } // index.ts import C, {type A, B} from "./constructions.ts"; // Ok // import type C, {type A, B} from "./constructions.ts"; // Error

Проверка наличия ECMAScript приватного поля

TYpeScript реализовал предложенный ECMAScript механизм проверки наличия приватного поля. Теперь, в теле класса при помощи оператора in можно производить проверки на присутствие одноименных приватных полей в других экземплярах. Но стоит сделать акцент, что идентификаторы проверяемых полей должны быть идентичны идентификаторам приватных полей объявленых в самом классе.

ts
class Person { #name: string; constructor(name: string) { this.#name = name; } /** * [*] Допустимы проверки только на одноименные * приватные поля объявленные в текущем классе! */ equals(other: unknown) { return other && typeof other === "object" && #name in other && // [*] this.#name === other.#name; } }

Поскольку с помощью оператора instanceof невозможно точно выявить принадлежность экземпляра из-за осуществления её на всем дереве иерархии...

ts
class A { } class B extends A { } class C extends B { } const c = new C(); console.log(c instanceof A); // [*] /** * [*] Невозможно однозначно интерпретировать экземпляр * при помощи оператора instanceof поскольку проверка * осуществляется по всей иерархии наследования. */

... а строковое представление класса не гарантирует место его объявления...

ts
console.log(c.constructor.name === `A`); // [*] /** * [*] это ./a/A.ts или ./b/A.ts? */

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

Утверждение импорта

Помимо прочего, TypeScript реализует предложенный ECMAScript механизм утверждения импорта.

ts
import config from "./config.json" assert { type: "json" };

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

ts
// Ok на этапе компиляции и Error во время выполенния import config from "./config.json" assert { type: "shmaip" };

Также подобный механизм может быть применен и к динамическому импорту.

ts
await import("./config.json", { assert: { type: "json" } });

Ускорение загрузки с помощью realPathSync.native

Раньше, функция realPathSync.native применялась только на Linux, но того, как nodejs реализовала её поддержку в остальных операционных системах, загрузка проектов стала осуществлятся на 5%-13% быстрее.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменения в lib.d.ts

Как всегда значительной переработке подверглись декларации входящии в групперовку lib.d.ts.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменения от вывода Awaited

Из-за введения в основную кодовую базу нового типа Awaited могут возникнуть при определении типа значения получаемого в асинхронных операциях.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Проверка корневых параметров комилятора в tsconfig.json

Возникновение ошибки при объявлении пустого поля верхнего уровня в tsconfig.json.

json
// tsconfig.json { "include": [] // Error -> пустой массив }