Що нового в TypeScript 5.0. Декоратори, покращення продуктивності та безпеки типізації

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Привіт! Мене звати Андрій, і я JavaScript-розробник у компанії Rolique. Як видно із статистики мов програмування DOU, популярність TypeScript стрімко зростає. І я можу це підтвердити, адже практично на всіх останні моїх проєктах використовувався TypeScript як основна мова програмування.

Нещодавно вийшла нова версія TypeScript 5.0. Тож давайте розберемося які основні зміни відбулися.

Цей реліз приніс багато нових функцій, при цьому, як стверджують розробники, метою було зробити TypeScript меншим, простішим та швидшим:

  • реалізували новий стандарт декораторів;
  • додали кращу підтримку ESM-проектів в Node та бандлерах;
  • покращили поведінку enum;
  • розширили функціональність JSDoc;
  • спростили конфігурацію та здійснили багато інших поліпшень.

Декоратори

Декоратори — це інструмент, який дозволяє розширювати поведінку класів, методів, властивостей об’єктів в декларативний спосіб, без зміни коду безпосередньо. Є висока ймовірність, що декоратори скоро стануть частиною стандарту ECMAScript, тому щоб зберегти сумісність, їх додали в TypeScript 5.0.

Розглянемо приклад:

class Person {
 name: string;
 constructor(name: string) {
   this.name = name;
 }


 greet() {
   console.log(`Hello, my name is ${this.name}.`);
 }
}


const p = new Person("Ron");
p.greet();

Метод greet досить простий у цьому випадку, але уявімо, що в ньому міститься якась складна логіка. Природно, що ми захочемо додати console.log, які за потреби дозволять віддебажити цей метод.

class Person {
 name: string;
 constructor(name: string) {
   this.name = name;
 }
 greet() {
   console.log("LOG: Entering method.");
   console.log(`Hello, my name is ${this.name}.`);
   console.log("LOG: Exiting method.")
 }
}

Такий підхід досить зручний і було б чудово мати можливість легко додавати логування і для інших методів. І тут нам у нагоді стають декоратори.

Декоратор — це функція, яка приймає target (оригінальний метод, який ми обгортаємо декоратором) і context (контекст виклику метода, який містить додаткову інформацію про «декорований» елемент, як його тип, ім’я і т. п.). Також ми повинні повернути нову функцію, в якій явно викликаємо target. Ми можемо додати логіку перед і після цього виклику.

function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
 const methodName = String(context.name);


 return function (this: any, ...args: any[]) {
   console.log(`LOG: Entering method '${methodName}'.`)
   const result = originalMethod.call(this, ...args);
   console.log(`LOG: Exiting method '${methodName}'.`)
   return result;
 };
}

Тепер ми можемо використати loggedMethod, щоб «декорувати» оригінальний метод greet:

class Person {
 name: string;
 constructor(name: string) {
     this.name = name;
 }

 @loggedMethod
 greet() {
     console.log(`Hello, my name is ${this.name}.`);
 }
}

const p = new Person("Ron");
p.greet();

// Output:
//
//   LOG: Entering method.
//   Hello, my name is Ron.
//   LOG: Exiting method.

Варто зазначити, що TypeScript у попередніх версіях підтримував «експериментальну» версію декораторів, яка вмикається флагом --experimentalDecorators. Вони були побудовані на основі старішої версії, тому не повністю сумісні з новими змінами. Та через те що функціональність була досить популярною (зокрема активно використовувалася такими інструментами як Angular і Nest.js), вона і далі певний час буде доступна з увімкненим флагом --experimentalDecorators. За замовчанням же будуть використовуватися «нові» ES-декоратори.

Якщо бажаєте детальніше ознайомитися з поточною версією ES-декораторів, рекомендую цю досить докладну статтю.

All enums Are Union enums

TypeScript має два типи enum: числовий та літеральний.

// числовий enum
// якщо значення неініціалізовані явно
// то вони ініціалізуються інкрементально
enum LogLevelNumeric {
 Debug = 1,
 Info, // 2
 Warn, // 3
 Error, // 4
}

// літеральний enum
// значення повинні бути явно ініціалізовані
// рядковими (String) або числовими (Number) літералами
enum LogLevelLiteral {
 Debug = "DEBUG",
 Info = "INFO",
 Warn = "WARN",
 Error = "ERROR",
}

Ці два типи enum мали різну поведінку у деяких випадках. Наприклад, у нас є функція, яка очікує параметром значення із enum. У випадку числового enum ми могли передати будь-яке число і це не викликало помилку компіляції.


enum LogLevel {
 Debug = 1,
 Info, // 2
 Warn, // 3
 Error, // 4
}

function log(level: LogLevel, message: string) {}

// Тут все гаразд
log(1, "Debugging message")

// Хоч у нас в LogLevel немає елемента ініціалізованого
// значенням 999, TypeScript попередніх версій не відобразить помилку
log(999, "It works also")

Якщо ж ми використовували літеральний enum, то для того, щоб передати параметр у функцію, ми повинні чітко передати конкретний елемент enum.


enum LogLevel {
 Debug = "DEBUG",
 Info = "INFO",
 Warn = "WARN",
 Error = "ERROR",
}

function log(level: LogLevel, message: string) {}

// У цьому випадку буде помилка
log("DEBUG", "It fails")

// Тут усе правильно
log(LogLevel.Debug, "Debugging message")

Починаючи з TypeScript 5.0, для кожного елементу enum буде створено свій тип і, відповідно, тип enum буде рівноцінний об’єднанню (Union) типів його елементів, незалежно від того чи це числовий, чи літеральний enum. Наприклад:


// Все ще працюватиме для числових enum
log(1, "Debugging message")

// У TypeScript 5.0 тут буде помилка!
log(999, "It fails in TypeScript 5.0")

// Для літеральних enum все ще потрібно передавати
// безпосередньо елемент
log(LogLevel.Debug, "Debugging message")

Ще одна зміна, яка стосується enum. Відтепер значення елементів enum можуть бути обчислені з використанням константних виразів для обох типів і неконстантних виразів — лише для числових enum. Наприклад, цей код не буде працювати у попередніх версіях TypeScript:


const baseValue = 10;
const prefix = "/data";
const enum Values {
   First = baseValue,  // 10
   Second,             // 11
   Third               // 12
}
const enum Routes {
   Parts = `${prefix}/parts`,       // "/data/parts"
   Invoices = `${prefix}/invoices`  // "/data/invoices"
}

const Type Parameters

При визначенні типу об’єкта, TypeScript зазвичай вибирає більш загальний тип. Наприклад, у цьому випадку типом для змінної names буде string[]:


type HasNames = { names: readonly string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
   return arg.names;
}

// Виведений тип: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

Це зроблено для того, щоб дозволити мутацію цього масиву нижче.

Однак, в залежності від того, що саме робить getNamesExactly та як він використовується, інколи нам корисно отримати більш конкретний тип.

До версії TypeScript 5.0 зазвичай рекомендувалося додавати конструкцію as const у певних місцях.


type HasNames = { names: readonly string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
   return arg.names;
}

// Виведений тип: string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

// Виведений тип:
//    readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);

Такий підхід незручний, можна легко забути про as const у потрібному місці. У TypeScript 5.0 тепер можна додавати модифікатор const до декларації параметра типу, щоб зробити константне приведення типом за замовчуванням:


type HasNames = { names: readonly string[] };

function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
//                       ^^^^^
   return arg.names;
}

// Виведений тип:
//    readonly ["Alice", "Bob", "Eve"]
// Немає потреби використовувати 'as const' тут
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

Варто зазначити, що модифікатор const не накладає обмежень імутабельності на значення, тому в прикладі нижче ми повинні використовувати модифікатор readonly для типу string[].


declare function fnBad<const T extends string[]>(args: T): void;

// 'T' приводиться до 'string[]' тому що тип 'readonly ["a", "b", "c"]' не сумісний з 'string[]'
fnBad(["a", "b" ,"c"]);

declare function fnGood<const T extends readonly string[]>(args: T): void;
//                                      ^^^^^^^^

// 'T' приводиться до readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);

Підтримка кількох конфігураційних файлів у extends

Працюючи з кількома проєктами корисно мати «базовий» файл конфігурації, який інші файли tsconfig.json можуть успадковувати. Для цього у файлі конфігурації є поле extends.


// packages/front-end/src/tsconfig.json
{
 "extends": "../../../tsconfig.base.json",
 "compilerOptions": {
     "outDir": "../lib",
     // ...
 }
}

Однак можуть бути випадки, коли ви захочете успадкувати конфігурацію одночасно з кількох файлів. Наприклад, уявіть, що ви використовуєте базовий файл конфігурації TypeScript, який поставляється через npm. Якщо ви хочете, щоб всі ваші проєкти також використовували параметри з пакета @tsconfig/strictest, то є просте рішення: додайте в поле extends базового файлу tsconfig.base.json значення @tsconfig/strictest:


// tsconfig.base.json
{
 "extends": "@tsconfig/strictest/tsconfig.json",
 "compilerOptions": {
     // ...
 }
}

Це буде працювати. Але якщо у вас є проєкти, в яких ви хочете також наслідувати налаштування з tsconfig.base.json, але водночас вам не потрібні конфігурації @tsconfig/strictest, то в цих проєктах потрібно або явно вимикати параметри, або створювати окрему версію tsconfig.base.json, яка не успадковує @tsconfig/strictest.

Щоб додати більше гнучкості, в Typescript 5.0 тепер дозволяється полям extends мати кілька записів. Наприклад, як у цьому файлі конфігурації:


{
 "extends": ["a", "b", "c"],
 "compilerOptions": {
     // ...
 }
}

У цьому випадку файл конфігурації розширюється конфігурацією c, яка в свою чергу розширюється конфігурацією b, а b — розширюється конфігурацією a. У випадку коли якісь налаштування «конфліктують», то застосовуються параметри тієї конфігурації, яка знаходиться правіше у списку.

У наступному прикладі strictNullChecks та noImplicitAny включені в кінцевому tsconfig.json.


// tsconfig1.json
{
 "compilerOptions": {
     "strictNullChecks": true,
     "noImplicitAny": false
 }
}

// tsconfig2.json
{
 "compilerOptions": {
     "noImplicitAny": true
 }
}

// tsconfig.json
{
 "extends": ["./tsconfig1.json", "./tsconfig2.json"],
 "files": ["./index.ts"]
}

Для демонстрації ми можемо переписати наш початковий приклад наступним чином:


// packages/front-end/src/tsconfig.json
{
 "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
 "compilerOptions": {
     "outDir": "../lib",
     // ...
 }
}

—moduleResolution bundler

В TypeScript 4.7 були додані node16 і nodenext опції для --module і --moduleResolution конфігурацій проєкту. Ці налаштування призначені для кращої підтримки ECMAScript модулів в Node.js. Проте ці режими мали певні обмеження. Наприклад, будь-які відносні імпорти ECMAScript модулів у Node.js вимагали вказувати розширення файлу.


import * as utils from "./utils";     //  неправильно

import * as utils from "./utils.mjs"; //  правильно

Але для багатьох розробників, які використовують такі інструменти як бандлери, налаштування node16/nodenext були громіздкими, оскільки бандлери в більшості не мають цих обмежень. Тому режим --moduleResolution node був кращим для тих, хто користувався бандлерами.

Проте режим --moduleResolution node вже застарів. Більшість сучасних бандлерів використовують поєднання ECMAScript і CommonJS модулів у Node.js. Щоб змоделювати як працюють бандлери, TypeScript додав нову стратегію: --moduleResolution bundler.

Якщо ви користуєтеся сучасним бандлерами, таким як Vite, esbuild, swc, Webpack, Parcel і т. п., які реалізовують гібридну стратегію, новий варіант bundler чудово вам підійде.

Проте, якщо ви пишете бібліотеку, щоб опублікувати її в npm, використання цього параметра може нести приховані проблеми сумісності, які можуть виникнути у ваших користувачів, що не використовують бандлер.

Покращення швидкодії, використання пам’яті і розміру пакета

Як стверджують розробники, TypeScript 5.0 містить багато суттєвих змін в структурі коду, структурах даних та реалізаціях внутрішніх алгоритмів. Це означає, що виконання і встановлення TypeScript тепер повинно бути швидшим.

Ось такі цифри (в порівнянні з TypeScript 4.9) наводяться в офіційному анонсі.

СценарійЧас або розмір порівняно з TS 4.9
material-ui build time90%
TypeScript Compiler startup time89%
Playwright build time88%
TypeScript Compiler self-build time87%
Outlook Web build time82%
VS Code build time80%
typescript npm Package Size59%

Як бачимо, TypeScript 5.0 має швидший час збирання проєкту приблизно на 10-20% в залежності від конкретних умов. При цьому розмір пакета TypeScript зменшився аж на 41% (з 63,8 мегабайт до 37,4 мегабайт).

Висновок

Отже, ми бачимо, що TypeScript активно підтримує сумісність із JavaScript і, додавши у новій версії декоратори, дозволяє використовувати можливості JS із «близького майбутнього» уже зараз. Також команда постійно працює над покращенням «безпеки» типів, що має тішити фанатів строгої типізації. Те, що окрім нових фіч, оптимізована робота самого TypeScript, показує, що мейнтейнерам проєкту не в останню чергу важливий і «користувацький досвід» розробників. Думаю, що це позитивно вплине і на так зростаючу кількість web-проєктів, в яких використовується ця мова програмування.

Це не вичерпний список змін, які зроблені у TypeScript 5.0. Крім згаданого, додані додаткові конфігурації, покращено підтримку JSDoc, сортування імпортів і автодоповнення для конструкції switch/case у редакторах коду. З цими змінами ви можете ознайомитися в офіціному анонсі TypeScript 5.0.

Дякую за прочитання! Віримо в перемогу і підтримуємо ЗСУ!

👍ПодобаєтьсяСподобалось19
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Декоратори — це інструмент, який дозволяє розширювати поведінку класів, методів, властивостей об’єктів в декларативний спосіб, без зміни коду безпосередньо.

Як на мене тут лажа. Декоратор, який тут наведено, змінює поведінку класу, коли в загальному розумінні декоратор повинен змінювати поведінку інстансу. Щось типу такого:

const p = new Person("Ron");
@loggedMethod
p.greet();

І в такому випадку я повинен протестувати тільки те, де цей інстанс використовується, а те що описано призводить до того, що я повинен протестувати всі місця де цей класс використовується, що, як правило, набагато набагато більше. Це синтаксичний цукор, а не декоратор. Прикольний цукор, але вибачте, термін вибраний зовсім невдало.

Доречі а якщо я хочу логувати один метод, але тільки в деяких випадках? Навіщо мені купа логів, які не відносяться до моєї проблеми? Як це зробити?

Так. Про декоратори, які застосовуються до інстансів, а не до классів

А якщо вже йде мова про Декоратори, а не те що в статті, то ось гарний приклад refactoring.guru/...​orator/typescript/example

GOF патерн Декоратор і пропозал декораторів у Джаваскрипті то як свинка і морська свинка

як на рахунок цього

Про декоратори, які застосовуються до інстансів, а не до классів

?
До чого застосовується патерн Декоратор GOF?

До чого взагалі ГОФ Декоратор до теми статті? Це зовсім інше, просто назва одна і та ж.

const p = new Person("Ron");
@loggedMethod
p.greet();

Ось таке у якій мові ти бачив?

Давай ви все ж таки визначитесь. Я запитав:

Ви про

Ви написали

Про декоратори, які застосовуються до інстансів, а не до классів

Я привів приклад

Декоратор GOF

Він застосовується до інстансу. Так?

просто назва одна і та ж.

Слава алаху, ви допетрали. Про це я і пишу — вона дуже невдала. Більше того я написав, що то хрінь, а не декоратор(я маю на увазі не патерн GOF). Типу клас ми не змінємо, але чомусь треба тепер нам перетестувати всі місця де у нас цей клас, а не конкретне місце, де я створив інстанс, задекорував його і отримав нову поведінку.

Слава алаху, ви допетрали.

Правда, важко жити, коли навколо усі дурні, а ти один розумний?

Він застосовується до інстансу. Так?

Так, але воно не має відношення до того, що у статті описано.

Більше того я написав, що то хрінь, а не декоратор(я маю на увазі не патерн).

Чому ви вирішили, що назва «декоратор» навічно заброньована за патерном від GOF? Наприклад, у Пайтоні, якщо я не помиляюсь, є декоратори, аналогічні пропозалу у Джаваскрипті. І щось мені підказує, що вони більш популярні, ніж патерн від ГОФу.

Правда, важко жити, коли навколо усі дурні, а ти один розумний?

Треба спочатку читати уважно, що пишуть, а потім бігати з коментарями.

Ось таке у якій мові ти бачив?

Не бачив, але було б класно таке мати

Та для цього не потрібні декоратори:

function decorate(otherFn) {

  return function () {
       console.log("before");
       const result = otherFn();
       console.log("after");
       return result;
  }
}

const decorated = decorate(p.greet.bind(p));
decorated();

а як працювати з інстансом в методі decorate?

ага, а як же

Та для цього не потрібні декоратори:

?

Ну якщо дуже треба можна додати другий параметр у decorate або забайндити його до p замість байнда самого метода.

додати другий параметр у decorate

як це захистить, якщо в мене два інстанса одного класу?

абайндити його до p замість байнда самого метода

і скільки мені потрібно різних декораторів, якщо я хочу залогувати 5 методів?

як це захистить, якщо в мене два інстанса одного класу?

Приклад того, що я навів повертає нову функцію, а не змінює інстанс.

і скільки мені потрібно різних декораторів, якщо я хочу залогувати 5 методів?

Один

Приклад того, що я навів повертає нову функцію, а не змінює інстанс.

огорнув я метод інстансу 1, а передав параметром інстанс 2.
Що буде?

Один

Як це буде виглядати?

Погоджуюся з Сергієм, що «класичний» патерн декоратор і декоратори ECMAScript, це трохи різні речі і обидва концепти мають право на існування.

Дійсно, якщо вам не потрібно логувати інформацію у всіх випадках, то ES-декоратор вам не підходить. Тому тут залежить від конкретної ситуації.

Наведений вами приклад застосування патерну декоратор також має свої обмеження, адже для того щоб «обгортати» різні інстанси одним декоратором — потрібно, щоб ці інстанси реалізовували один інтерфейс. В свою чергу, ES-декоратор, наприклад методу, можна застосовувати до різних методів різних класів. Такий собі спосіб перевикористання логіки.

це трохи різні речі і обидва концепти мають право на існування.

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

«обгортати» різні інстанси одним декоратором

різні класи, а не інстанси

Декоратори — це інструмент, який дозволяє розширювати поведінку класів, методів, властивостей об’єктів в декларативний спосіб, без зміни коду безпосередньо

Це маніпуляція. Ці декоратори призводять до перетестування всіх(!) міст, де цей клас використовується, бо є якраз зміна коду класу.

const methodName = String(context.name);

а нащо тут каст в стрінг? хіба в контексті не стрінг лежить?

context.name має тип string | symbol

Якщо context.name явно не привести до рядка, то буде помилка про те що неявне приведення symbol в string викличе помилку в рантаймі.

Це просто переклад презентантії нових фіч від команди TS? Навіть приклади ті самі, дуже цікава стаття...

Ну не самому ж писати з нуля)
На укр переклав і вже норм ;-)

чекаємо статті від вас ;)

Нема питать)
Починаю писати.
Чекайте ;-)

може ви напишете статтю?)

ні, я занадто лінивий, і вмію тільки вимагати чогось від інших людей😁

Так, більшість прикладів взяті з анонсу (але не всі 🙂), ну і це ж не дослівний переклад (але кого то цікавить 😂).

В будь-якому випадку, дякую за критику. Я це врахую.

Унікальний випадок в індустрії)
Щоб писати на одній мові потрібно знати дві)

Фактично TypeScript є «superset» JavaScript і зберігає зворотню сумісність, додаючи зверху свої «фічі». Тому не бачу особливої проблеми.

Супер то сет)
А окрема дока є — будьте ласкаві вивчить)
А потім макаки нагенерять типів які без пляшки не розібрати використовуючи всі вигини мови і сиди ябайся)

«Палка має два кінці» 🙂 Ті ж самі макаки можуть і на JS так код написати, що потім довго можна в рантаймі помилки виловлювати. Тому я за здоровий глузд і використання інструменту лише там, де він принесе більше користі ніж шкоди 😂

TypeScript є «superset» JavaScript і

То досить розповсюджена помилка.

Бо то далеко не superset, a someset. Причому someset з відсутньою сеціфікацією.

А можна приклад? Код на джаваскрипті, який не запрацює у тайпскрипті? (Звісно, з урахуванням заявленой підтримки тайпскриптом версіі джс-а)

Якщо обернути в Number, то все запрацює

повинно щоб працювало без Number, як в js, якщо не працює то ts це не superset js

А щоб писати на С++ потрібно знати С?

так плюсы это улучшеный си, это одно и тоже, только один из них с ооп

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

Так то і у тайпскрипті можна писати просто джаваскрипт код.

Точнее, вырос из си. А то улучшений больше чем самого си.

На С++ можно писать в очень разном стиле. Программисты 7-го разряда и то знают C++ на 50%. Целиком его не знает никто.

Підписатись на коментарі