Що нового у TypeScript 4.1
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Мене звуть Романа Мадай, я Senior Front End developer в Intellias. У цій статті розглянемо актуальну версію TypeScript 4.1, яка вийшла у листопаді 2020 року, та основні новвоведення, які в ній з’явились. Також нещодавно представили beta версію TypeScript 4.2, реліз якої очікується в лютому.
Останні кілька років популярність TypeScript зростає — це добре видно зі статистики популярності мов у 2020 році (згідно з даними GitHub), де TypeScript входить у п’ятірку найбільш перспективних. Близько 60% програмістів JS вже використовують TypeScript у розробці своїх проектів, а 22% бажають спробувати. За даними Stack Overflow 2020 року, TypeScript подобається розробникам і займає 2 позицію.
До цього релізу були додані: корисні функції, нові опції перевірки, удосконалення продуктивності редактора та покращення швидкодії.
Роглянемо основні нововведення.
1. Template Literal Types
Очікуване нововведення в TypeScript 4.1 — Template Literal Types — надає можливість використовувати звичайні типи літеральних рядків як визначення для інших типів. Вони вже набули широкої популярності серед користувачів, які активно діляться своїми прикладами використання, включно з querySelector, парсинг роутів, роботою з JSON, перевіркою SQL-запитів, парсинг CSS, перевіркою орфографії тощо.
До TypeScript 4.1 ми мали три літеральні типи: string, number і boolean. TypeScript 4.1 представив четвертий тип літералу: template literal (шаблонний літерал).
Нам добре відома особливість ES6 — Templates Literal, який реалізується використанням зворотних лапок.
const hello = 'Hello'; const message = `${hello} World`;
Ця функціональність набула популярності через покращення синтаксису, простий спосіб інтерполяції змінних та виразів в рядок.
Template Literal Types має такий самий синтаксис, як у Template Literal String в JS, але використовується в позиціях типу. Коли ви використовуєте його з конкретними літеральними типами, він створює новий тип рядкового літералу, об’єднуючи вміст.
type Color = 'red'; type Animal = `${Color} cat`; // same as // type Animal = 'red cat';
Template Literal Types також працює із Union types*:
type Color = 'red' | 'black'; type Size = 'small' | 'big'; type Cat = `${Color | Size} cat`; // same as // type Cat = "red cat" | "black cat" | "small cat" | "big cat";
*Union types — це тип даних, який створюється на основі комбінування декількох інших типів даних.
Даний підхід можна використовувати й для більш складних задач, наприклад, для побудови нового типу рядкового літерала конкатенацією вмісту.
type VerticalAlignment = 'top' | 'middle' | 'bottom'; type HorizontalAlignment = 'left' | 'center' | 'right'; // | 'top-left' | 'top-center' | 'top-right' // | 'middle-left' | 'middle-center' | 'middle-right' // | 'bottom-left' | 'bottom-center' | 'bottom-right' declare function setAlignment(value:`${VerticalAlignment}-${HorizontalAlignment}`): void; setAlignment('top-left'); // works! setAlignment('top-middle'); // error! setAlignment('top-bottom'); // error!
В рамках даного функціоналу були додані нові утилітарні типи для маніпуляції рядками — Uppercase, Lowercase, Capitalize, Uncapitalize.
type UppercaseGreeting<Str extends string> = `${Uppercase<Str>}` type Greeting = UppercaseGreeting<"hello"> // same as // type Result = "HELLO" type LowercaseGreeting<Str extends string> = `${Lowercase<Str>}` type Greeting = LowercaseGreeting<"HELLO"> // same as // type Result = "hello" type LowercaseGreeting = "hello, world"; type Greeting = Capitalize<LowercaseGreeting>; // same as // type Greeting = "Hello, world" type LowercaseGreeting = "HELLO WORLD"; type Greeting = Uncapitalize<LowercaseGreeting>; // same as // type Greeting = "hELLO WORLD"
Template Literal дає можливість синтаксично аналізувати параметри маршруту у веб фреймворках Node.js, таких як Express:
app.get("/users/:userId", (req, res) => { const { userId } = req.params; res.send(`User ID: ${userId}`); });
Template Literal — це потужна функція, не така складна у використанні, як може здатися на перший погляд. Часто це розбиття та об’єднання рядків на основі простих умов. Проте, не слід зловживати їх використанням, оскільки з часом вони нагромаджуються та стають досить складними, особливо в поєднанні з рекурсією.
2. Key Remapping in Mapped Types
Mapped Type може створювати нові типи об’єктів на основі довільних ключів. Рядкові літерали в таких типах можуть бути задіяні в якості імен властивостей.
type Actions = { [K in 'canEdit' | 'canCopy' | 'canDelete']?: boolean }; // same as // type Actions = { // canEdit?: boolean, // canCopy?: boolean, // canDelete?: boolean // };
Або нові типи об’єктів на основі інших типів об’єктів.
/// 'Partial<T>' - те саме, що і 'T', але з кожною властивістю, позначеною як не обов'язкова. type Partial<T> = { [K in keyof T]?: T[K] };
Новий специфікатор as дозволяє використовувати типи рядкових літералів, легко створювати імена нових властивостей на основі вже наявних. Ключі можна фільтрувати за допомогою never, що в деяких випадках позбавляє необхідності використовувати допоміжну інструкцію Omit.
Застосування типів рядкових літералів зі специфікатором as:
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } interface Person { name: string; age: number; location: string; } type LazyPerson = Getters<Person>; // same as // type LazyPerson = { // getName: () => string; // getAge: () => number; // getLocation: () => string; // } // Remove property 'kind' type RemoveKindField<T> = { [K in keyof T as Exclude<K, 'kind'>]: T[K] }; interface Circle { kind: 'circle'; radius: number; } type KindnessCircle = RemoveKindField<Circle>; // same as // type KindnessCircle = { // radius: number; // }
3. Recursive Conditional Types
Ще одне нововведення робить обробку Conditional Types* більш гнучкою, дозволяє їм посилатися на самих себе в своїх відповідях. Наприклад, розгортання глибоко вкладеного проміса за допомогою Awaited:
T extends U ? T1 : T2 type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T; /// Схоже до `promise.then(...)`, але точніші за типами. declare function customThen<T, U>( p: Promise<T>, onFulfilled: (value: Awaited<T>) => U ): Promise<Awaited<U>>;
*Умовні типи (Conditional Types) — це типи, що здатні приймати одне з двох значень, базуючись на виразі, в якому встановлюється приналежність до заданого типу даних. Умовні типи семантично схожі з тернарним оператором.
Потрібно мати на увазі, що ці рекурсивні типи є потужним інструментом, проте ними слід користуватися відповідально, оскільки це може негативно вплинути на швидкість перевірки типів.
4. Checked Indexed Accesses
Сигнатури індексів в TypeScript дозволяють звертатися до властивостей з довільним ім’ям, як показано в інтерфейсі User. Тут ми бачимо, що властивість, в якій не вказано name або username, повинна мати тип string.
interface User{ name: string; username: string; // Ця сигнатура індексу отримує додаткові властивості. [propName: string]: string; } function checkUser(user: User) { user.name; // string user.username; // string // Це вже також дозволено // мають тип 'string'. user.email.toString(); } const user: User = { name: "Bob", username: "Ross" };
Будь-яка властивість, що доступна для User, крім наявних (name, username), матиме тип string. Проблема в тому, що будь-який доступ до властивостей може бути undefined, але це не відображається на типі. Наприклад, у наведеному вище блоці user.email потенційно не визначений, але тип — string.
function checkUser(user: User): void { console.log(user.name) // string console.log(user.username) // string console.log(user.email.toString()) // Помилка якщо noUncheckedIndexedAccess встановлено // ~~~~~~~~~~~~~ Object is possibly 'undefined'. }
TypeScript пропонує нам рішення, нова опція — noUncheckedIndexedAccess, що активує режим, в якому при зверненні до властивості (наприклад, user.email) потрібно виконати перевірку його наявності або використовувати оператор not-null (!).
// Спочатку перевірка наявності властивості if (user.email) { console.log(user.email.toString()); } // Або за допомогою оператора '!' user.email!.toString();
Опція - -noUncheckedIndexedAccess допомагає виявити помилки, але не активується з опцією - -strict автоматично, оскільки змінює поведінку в розповсюджених сценаріях, наприклад, при переборі масиву.
let arr = [1, 2, 3, 4]; for (let i = 0; i < arr.length; i++) { const n: number = arr[i]; // Error // Type 'number | undefined' is not assignable to type 'number'. console.log(n); }
5. paths без baseUrl
В попередніх версіях TypeScript для використання paths в файлі tsconfig.json доводилося оголошувати параметр baseUrl. В поточній 4.1 допускається визначення paths без цього параметру. Таким чином вирішується проблема неправильних шляхів при автоматичних імпортах.
Шляхи і baseUrl в tsconfig.json:
{ "compilerOptions": { "baseUrl": "./src", "paths": { "@shared": ["@shared/"] // Відноситься до //"baseUrl" } } }
6. checkJs означає allowJs
JavaScript проекти, де використовується опція checkJs для реєстрації помилок в файлах .js, також повинні мати allowJs, щоб ці файли компілювалися. У TypeScript 4.1 такої необхідності більше немає, оскільки тепер checkJs передбачає allowJs за замовчуванням.
checkJs і allowJs в tsconfig.json:
{ "compilerOptions": { "allowJs": true, "checkJs": true } }
7. React 17 Фабрики JSX
JSX — це технологія, яка була представлена в React. JSX дозволяє писати HTML-елементи в JS і розміщувати їх в DOM без участі методів createElement (), appendChild ().
ReactDOM.render( <div id="test"> <h1>A title</h1> <p>A paragraph</p> </div>, document.getElementById('myapp') )
TypeScript 4.1 підтримує фабричні функції jsx та jsxs React v.17 завдяки двом новим опціям для параметру компілятора jsx:
react-jsx react-jsxdev
З заміток TypeScript: «Ці опції призначені для prod компіляції та компіляції в середовищі dev відповідно. При цьому часто буває, що одна розширює іншу».
Ось два приклади з документації, що описують конфігурацію для prod і dev:
tsconfig.json — приклад конфігурації TS для prod:
// ./src/tsconfig.json { "compilerOptions": { "module": "esnext", "target": "es2015", "jsx": "react-jsx", "strict": true }, "include": ["./**/*"] }
tsconfig.dev.json — приклад конфігурації TS для збірок середовища dev:
// ./src/tsconfig.dev.json { "extends": "./tsconfig.json", "compilerOptions": { "jsx": "react-jsxdev" } }
8. Підтримка редакторами JSDoc @see Tag
Тег @see тепер має кращу підтримку в редакторах TypeScript та JavaScript, що робить написання коду зручнішим.
Використання тега @see (із документації):
// @filename: first.ts export class C {} // @filename: main.ts import * as first from "./first"; /** * @see first.C */ function related() {}
9. Breaking Changes
Зміни lib.d.ts
Спеціальний файл lib.d.ts створюється з кожною інсталяцією TypeScript. Цей файл містить декларації зовнішнього середовища для різних типових JavaScript конструкцій, присутніх у середовищах виконання JavaScript та DOM.
Завдання цього файлу — полегшити написання JavaScript коду та перевірка типів. Ви можете не включати цей файл в контекст компіляції, вказавши опцію - -noLib (або «noLib»: true у tsconfig.json):
const foo = 111; const bar = foo.toString();
Немає помилки перевірки типу, оскільки функція toString визначена у lib.d.ts для всіх об’єктів JavaScript.
Якщо використати той самий зразок коду з опцією noLib, буде помилка перевірки типу:
const foo = 111; const bar = foo.toString(); // ERROR: Property 'toString' does not exist on type 'number'.
abstract методи не можна позначати як async
Методи, позначені як abstract, більше не можуть бути позначені як async. Для корекції коду потрібно видалити ключове слово async.
abstract class BaseClass { abstract async foo(): Promise<number> // Error }
any/unknown розповсюджуються на помилкові позиції
Раніше для такого виразу, як foo && somethingElse, тип foo був any або unknown, а тип цілого виразу був би типом somethingElse.
Наприклад, раніше типом x був {someProp: string}.
declare let foo: unknown; declare let somethingElse: { someProp: string }; const x = foo && somethingElse;
В TypeScript 4.1 ми обережніше ставимося до того, як визначаємо цей тип. any і unknown поширюються не на тип в правій частині, а назовні. Зазвичай виправити це можна зміною foo && someExpression на !! foo && someExpression. Подвійний знак оклику (!!) є скороченим способом приведення змінної до логічного значення (true або false).
Параметри для resolve в Promise тепер обов’язкові
new Promise(resolve => { doSomethingAsync(() => { doSomething(); resolve(); }); });
Цей приклад видасть помилку:
resolve() ~~~~~~~~~ error TS2554: Expected 1 arguments, but got 0. An argument for 'value' was not provided.
Щоб це виправити, в resolve проміса необхідно передати не менше одного значення, або оголосити Promise з явним аргументом загального типу void у випадках, коли виклик resolve() потрібно зробити без аргументу.
new Promise<void>(resolve => { doSomethingAsync(() => { doSomething(); resolve(); }); };
Умовні оператори розповсюдження (spread) створюють необов’язкові властивості
Для розкладання об’єкта в JS використовують spread оператор.
let Fruits = { f1:'Apple', f2:'Pineapple', f3:'Orange' } let Fruits2 = { ...Fruits } // Fruits2 ={ f1:'Apple', f2:'Pineapple', f3:'Orange' }
Проте оператор spread не працює для невизначених значень, і вони будуть пропущені:
let Fruits = { f1:'Apple', f2: undefined, f3:'Orange' } let Fruits2 = { ...Fruits } // Fruits2 ={ f1:'Apple', f3:'Orange' }
У наведеному прикладі, якщо pet визначено, то і властивості pet.owner будуть розповсюджені, в іншому випадку жодні властивості не будуть розповсюджені на повернутий об’єкт.
interface Person { name: string; age: number; location: string; } interface Animal { name: string; owner: Person; } // Можна використати optional chaining: function copyOwner(pet?: Animal) { return { ...(pet?.owner), otherStuff: 123 } }
До виходу релізу TypeScript 4.1, тип повернення для copyOwner був типом об’єднання на основі кожного розповсюдження:
{ x: number } | { x: number, name: string, age: number, location: string }
Такий підхід може приводити до витрат ресурсів, при сотнях розповсюджень в одному обєкті, кожен з них потенційно додає сотні або тисячі властивостей. Тому для підвищення продуктивності в TypeScript 4.1, тип, що повертається, використовує всі властивості як optional.
{ x: number; name?: string; age?: number; location?: string; }
Це в кінцевому підсумку працює швидше і краще відображається.
Невідповідні параметри більше не пов’язуються
Раніше TypeScript зв’язував параметри, які не відповідають один одному, приводячи їх до типу any. З виходом нового релізу 4.1 TypeScript тепер повністю пропускає цей процес.
let suits = ["hearts", "spades", "clubs", "diamonds"]; function pickCard(x: { suit: string; card: number }[]): number; function pickCard(x: number): { suit: string; card: number }; function pickCard(x: any): any { // Перевірка чи це об'єкт чи масив // Якщо об'єкт, отримуємо колоду і вибираємо карту if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // В іншому випадку вибір карти робить функція else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; } } let myDeck = [ { suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }, ]; let pickedCard1 = myDeck[pickCard(myDeck)]; console.log(`card: ${pickedCard1.card} of ${pickedCard1.suit}`); let pickedCard2 = pickCard(15); console.log(`card: ${pickedCard2.card} of ${pickedCard2.suit}`);
Висновок
На мою думку, з усіх оновлень в TypeScript 4.1 найбільш корисним та очікуваним є оновлення Template Literal Types— тепер ми можемо визначати типи більш гнучко. Також, важливим оновленням є Key Remapping in Mapped Types, що дозволяє створювати нові типи на основі довільних ключів. Ці новвоведення надають девелопменту зручності, що є важливим фактором у процесі розробки. Приємно бачити розвиток TypeScript, команда працює в правильному напрямку, вже скоро чекаємо на наступну версію.
22 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів