5 корисних фіч TypeScript 3.7 та способи їх застосування

1 хв. читання

Реліз TypeScript 3.7 очікується вже незабаром — 5 листопада, і він обіцяє бути насиченим новими фічами. Серед них:

  • assert-сигнатури;
  • аліаси для рекурсивних типів;
  • await верхнього рівня;
  • оператор null-об'єднання;
  • опціональне зв'язування.

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

Assert-сигнатури

// В JS:

function assertString(input) {
    if (typeof input === 'string') return;
    else throw new Error('Input must be a string!');
}

function doSomething(input) {
    assertString(input);

    // ... Тепер можна бути впевненим, що значення змінної – string
}

doSomething('abc'); // Все ок
doSomething(123); // Викидає помилку

Дуже акуратний та корисний патерн, однак поки що недоступний в TypeScript.

TypeScript не може знати гарантований тип input після того, як виконає assertString. Зазвичай розробники суворо вказують тип для параметра (input: string), і це хороша практика, однак так можна отримати проблему з типами десь в іншому місці коду. Саме в таких ситуаціях і корисна нова фіча.

// З TS 3.7

function assertString(input: any): asserts input is string { // <-- магія
    if (typeof input === 'string') return;
    else throw new Error('Input must be a string!');
}

function doSomething(input: string | number) {
    assertString(input);

    // тільки тут дізнаємося, що тип input — 'string' 
}

Тут вираз assert input is string дозволяє нам переконатися, що якщо функція повертає значення, TypeScript зміг привести тип input до string.

Щоб убезпечити такий код при хибному результаті assert-виразу, функція викине помилку, або ж нічого не поверне.

Ми навели дуже простий приклад, однак нова фіча може бути ще потужнішою:

// З TS 3.7

// Припускаємо, що input має правдиве значення або викидаємо помилку:
function assert(input: any): asserts input { // <-- не помилка
    if (!input) throw new Error('Not a truthy value');
}

declare const x: number | string | undefined;
assert(x); // Приводить x до number | string

// Також можливе використання з перевіркою типу 
assert(typeof x === 'string'); // Приводить x до string

// -- Або використовуйте assert у ваших тестах: --
const a: Result | Error = doSomethingTestable();

expect(a).is.instanceOf(result); // 'instanceOf' може припускати 'asserts a is Result'
expect(a.resultValue).to.equal(123); // тепер можна звернутися до a.resultValue

// -- Така конструкція викине помилку одразу, якщо ви передали некоректне значення --
function assertDefined<T>(obj: T): asserts obj is NonNullable<T> {
    if (obj === undefined || obj === null) {
        throw new Error('Must not be a nullable value');
    }
}
declare const x: string | undefined;

// Призначивши y 'string' як тип, можемо отримати помилку десь пізніше:
const y = x!;

// Призначивши y 'string' як тип, викинемо помилку одразу, якщо щось пішло не так:
assertDefined(x);
const z = x;

// -- Або навіть можемо оновлювати типи, щоб відстежувати сторонні ефекти функцій --
type X<T extends string | {}> = { value: T };

// Використовуйте asserts, щоб привести типи відповідно до сторонніх ефектів:
function setX<T extends string | {}>(x: X<any>, v: T): asserts x is X<T> {
    x.value = v;
}

declare let x: X<any>; // x тепер { value: any };

setX(x, 123);
// x тепер { value: number };

Конструкції з цього фрагмента ще на етапі розробки, тому стежте за пул-реквестами.

Розробники сперечаються, чи варто функціям використовувати assert та повертати тип, оскільки нам доведеться відстежувати набагато більше сторонніх ефектів.

await верхнього рівня

Конструкція async/await робить роботу з промісами набагато чистішою і приємнішою. Однак ми не можемо використовувати їх на верхньому рівні. Якщо це TS-бібліотека, вам не варто турбуватися, однак зовсім інша справа, якщо ви пишете виконуваний скрипт або використовуєте TypeScript у REPL. До того ж await верхнього рівня чудово функціонує у консолі Chrome та Firefox вже декілька років.

На щастя, скоро ми отримаємо фікс такої проблеми. Як це буде працювати:

// Сьогодні:

// Єдиний варіант для функції, що робить щось асинхронне
async function doEverything() {
    ...
    const response = await fetch('https://web.archive.org/web/20230605160853/http://example.com');
    ...
}
doEverything(); // Можна було б використати IIFE 

З await верхнього рівня:

// З TS 3.7:

// Ваш скрипт:
...
const response = await fetch('https://web.archive.org/web/20230605160853/http://example.com');
...

Варто звернути увагу: якщо ви не пишете скрипт, або не використовуєте REPL, не застосовуйте таку конструкцію (хіба що ви справді на 100% впевнені у результаті).

Ви запросто можете використовувати нову фічу, щоб писати модулі, які блокують асинхронні виклики при імпорті. Для деяких особливих випадків це корисно, однак більш звично, коли вирази import синхронні, надійні та досить швидкі операції. Ви можете легко скоротити час запуску вашого коду, якщо почнете блокувати складні асинхронні процеси (або ті, що можуть викинути помилку).

Ситуація дещо пом'якшується семантикою імпортів асинхронних модулів: вони імпортуються та виконуються паралельно, тому імпортований модуль чекає, поки виконається Promise.all(importedModules), а потім виконується сам.

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

Зазначимо, що нова фіча стане у пригоді, лише якщо існує підтримка асинхронних імпортів. Досі немає формальної специфікації щодо того, як TS буде обробляти таку конструкцію. Та дуже ймовірно, що як остання цільова конфігурація, так і ES модулі чи Webpack v5 (альфа-версія вже має експериментальну підтримку) — в рантаймі.

Аліаси для рекурсивних типів

Якщо ви колись мали справу з оголошенням рекурсивних типів у TypeScript, то могли натрапити на подібне запитання на StackOverflow.

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

Для конкретного прикладу візьмемо запропоноване оголошення типу JSON:

// Зараз:

type JSONValue =
    | string
    | number
    | boolean
    | JSONObject
    | JSONArray;

interface JSONObject {
    [x: string]: JSONValue;
}

interface JSONArray extends Array<JSONValue> { }

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

Щоб виправити таку незручність, не потрібен новий синтаксис, код компілюватиметься вже у TS 3.7:

// З TS 3.7:

type JSONValue =
    | string
    | number
    | boolean
    | { [x: string]: JSONValue }
    | Array<JSONValue>;

Наразі такий код викине помилку Type alias 'JSONValue' circularly references itself.

Оператор null-об'єднання

Попри складність назви, це один з найпростіших операторів. Він пов'язаний з JavaScript stage-3 пропозицією, а це означає, що ми також побачимо цю фічу і у ванільному JavaScript.

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

// Сьогодні:

// Використовуємо перший результат з firstResult/secondResult, який буде правдивим:
const result = firstResult || secondResult;

// Використовуємо configValue, якщо це значення існує в options, в іншому випадку – 'default':
this.configValue = options.configValue || 'default';

Цей трюк корисний в багатьох випадках, але іноді через дивну поведінку JavaScript ви можете не отримати бажаного результату. Якщо firstResult або options.configValue можуть отримувати такі значення як false, пустий рядок або 0, виникне баг. У JavaScript подібні значення вважаються хибними, тому відпрацьовуватиме запасне значення (secondResult / 'default').

З оператором null-об'єднання ми зможемо виправити такі незручності, записавши:

// With TS 3.7:

// Використовуємо перше визначене значення з firstResult/secondResult:
const result = firstResult ?? secondResult;

// Використовуємо configSetting з options, якщо його значення визначено, в іншому випадку –'default':
this.configValue = options.configValue ?? 'default';

Оператор ?? відрізняється від || тим, що пропускає значення, лише якщо воно має тип null чи undefined, але не хибне значення. Тобто якщо ви передасте false як firstResult, то ми все одно будемо використовувати це значення, адже воно визначене.

Все просто, однак дуже корисно, адже позбавляє нас багатьох багів.

Опціональне зв'язування

І наостанок розглянемо ще одну stage-3 пропозицію, яка знайде своє застосування і в TypeScript.

Нова фіча розв'яже проблему, яка спіткає розробників в кожній мові програмування: як отримати дані зі структури даних, якщо частини або всіх даних немає?

Зараз ви робите щось подібне:

// Сьогодні:

// Щоб отримати data.key1.key2, якщо будь-який рівень може бути null/undefined:
let result = data ? (data.key1 ? data.key1.key2 : undefined) : undefined;

// Як альтернатива
let result = ((data || {}).key1 || {}).key2;

Код стає набагато гіршим, якщо маємо справу з довшим ланцюжком властивостей. Ба більше, другий варіант навіть не скомпілюється у TypeScript, оскільки на першому кроці можемо отримати {}, а тоді key1 буде зовсім не валідним ключем.

Все ще більше ускладнюється, якщо вам треба отримати значення з масиву, або зробити виклик функції.

Є безліч варіантів обробки такого коду, але всі вони громіздкі та ненадійні. З опціональним зв'язуванням ви можете робити так:

// З TS 3.7:

// Повертаємо значення, якщо попередні властивості визначені та не дорівнюють null, в іншому випадку – undefined
let result = data?.key1?.key2;

// Те ж саме з масивами:
array?.[0]?.['key'];

// Викликаємо метод, але лише якщо він визначений
obj.method?.();

// Отримуємо властивість або повертаємо 'default', якщо на будь-якому кроці є невизначене значення
let result = data?.key1?.key2 ?? 'default';

В останньому прикладі можна побачити, який чистий код ми отримуємо при поєднанні двох фіч: оператору null-об'єднання та опціонального зв'язування.

Однак варто звернути увагу: така конструкція повертатиме undefined для не присвоєних значень, навіть якщо вони типу null. Тобто у випадку (null)?.key повернеться undefined. Тож варто стежити за цим, якщо ваша структура даних має багато null-значень.

Насправді в TypeScript 3.7 очікується ще багато корисних фіч, фіксів та покращень, з усіма ними можна ознайомитись за посиланням.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація