4.5
Поддержка ECMAScript модулей в NodeJs
Начиная с текущей версии TypeScript реализует поддержку ECMAScript (ESM) модулей в среде nodejs. Для активации данного функционала, в tsconfig.json
необходимо задать свойству module
значение nodenext
.
{ "compilerOptions": { "module": "nodenext", } }
Основные правила придерживаются спецификации ECMAScript, но существует один момент, о котором стоит упомянуть.
В случае активации esm в package.json
и выборе модулей nodenext
относительные пути в импортах требуют уточнение расширения, как .js
. Другими словами импорт файла file.(ts|tsx|js|jsx)
должен выглядить, как import "./file.js"
, а не import "./file"
.
// 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 модули таким образом, как если бы они имели экспор по умолчанию.
// file f.сts export const f = () => {}; // file index.ьts import f from "./f";
Кроме этого, после реализации esm
nodejs, в package.json
было добавлено новое свойство exports
позволяющее конкретизировать точки входа.
// package.json // package.json { "type": "module", "exports": { ".": { "import": "./esm/index.js", "require": "./commonjs/index.cjs", }, }, // для поддержки старых версий "main": "./commonjs/index.cjs", }
Таким образом, TypeScript также добавляет новую возможность указания деклараций типов.
// 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 менеджера.
> npm i -D @types/web
// package.json { "devDependencies": { "@typescript/lib-dom": "npm:@types/web" } }
Тип Awaited
Новый расширенный тип Awaited<T>
предназначен для рекурсивного развертывания промисов, поможет без труда указывать тип, к которому принадлежит результат асинхронных операций.
// A = string type A = Awaited<Promise<string>>; // B = string type B = Awaited<Promise<Promise<string>>>; // C = string | number type C = Awaited<string | Promise<number>>;
Указание шаблонного литерального строкового типа в качестве дискриминанта
Начиная с текущей версии, в качестве дескременантного типа можно указывать шаблонный строковой литеральный тип.
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.
Исключение хвостовой рекурсии на условных типа
Безусловно, рекурсивные типы значительно упращают написание кода, но из-за ограничений связанных с бесконечным циклом не могут показать себя полность. Представьте, что необходимо создать тип, который будет удалять крайние пробелы у литерального строкогого типа.
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; type Test = TrimLeft<" text">; // type Test = "text"
Это очень простая задача, но реализуема только при условии ограниченной последовательности повторения пробелов. Простыми словами, если рекурсивная операция определение пробелов привысит установленный компилятором лимит, то возникнет ошибка.
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 вводит оптимизацию позволяющую нашему примру быть работоспособным.
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T; /** * Начиная с текущей версии [*] Ok! */ type Test = TrimLeft<" text">; // [*]
Отключение исключения неиспользуемого импорта
Не так давно, в TypeScript был реализован механизм предназначеный для исключения импортов для неиспользуемых констркций, но затем выяснилось, что в некоторых случаях, это поведение является нежелатильным. К примеру, известный фраймворк vuejs
выполняет импорт конструкций в теле тегов <script></script>
, а использует их в шаблоне описывающем структуру компонентов.
<script setup> import { handler } from "./handlers.ts"; </script> <button @click="handler">Нажми меня!</button>
Поскольку TypeScript проверяет исключительно блок в котором расположен код, импорт функции-слушателя будет исключон из сборке, что неизбежно приведет в ошибке во время выполнения.
Во избежания подобных ситуаций был введен новый флаг --preserveValueImports
, который отключает удаление неиспользуемого импорта. Единственный важный момент заключается в том, что его необходимо использовать вместе с флагом --isolatedModules
, которому задано значение true
.
Совмещение import type с обычным import
До недавнего времени импорт конструкций и импорт только типов, независимо от того, что они определены в одном файле, приходилось разбивать на несколько этапов.
// 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";
Начиная с текущей версии импорт конструкции и импорт только типов можно осуществлять в одной операции.
import {type A, B} from "./constructions.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
можно производить проверки на присутствие одноименных приватных полей в других экземплярах. Но стоит сделать акцент, что идентификаторы проверяемых полей должны быть идентичны идентификаторам приватных полей объявленых в самом классе.
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
невозможно точно выявить принадлежность экземпляра из-за осуществления её на всем дереве иерархии...
class A { } class B extends A { } class C extends B { } const c = new C(); console.log(c instanceof A); // [*] /** * [*] Невозможно однозначно интерпретировать экземпляр * при помощи оператора instanceof поскольку проверка * осуществляется по всей иерархии наследования. */
... а строковое представление класса не гарантирует место его объявления...
console.log(c.constructor.name === `A`); // [*] /** * [*] это ./a/A.ts или ./b/A.ts? */
... механизм определения класса по приватным полям обозначается, как эргонамичная проверка бренда, поскольку только приватные поля отличают экземпляры в одном иерархическом древе.
Утверждение импорта
Помимо прочего, TypeScript реализует предложенный ECMAScript механизм утверждения импорта.
import config from "./config.json" assert { type: "json" };
Стоит уточнить, что TypeScript просто компилирует утверждения втом виде, в котором его указал разработчик, перекладывая ответственность за проверки на срежду выполнения кода. Поэтому при использовании данного механизма нужно быть придельно внимательным.
// Ok на этапе компиляции и Error во время выполенния import config from "./config.json" assert { type: "shmaip" };
Также подобный механизм может быть применен и к динамическому импорту.
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
.
// tsconfig.json { "include": [] // Error -> пустой массив }