Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Що нового у 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, команда працює в правильному напрямку, вже скоро чекаємо на наступну версію.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному8
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

По якісь причині не працює Template Literal Types, коли я намагаюсь його використати у Key Remapping in Mapped Types.

Спочатку створюю просто Template Literal Types, тут все працює як очікується:

type FieldPattern = `/${string}`;
const path1: FieldPattern = '/one'; // OK
const path2: FieldPattern = 'two'; // Type '"two"' is not assignable to type '`/${string}`'.ts(2322)

Але чомусь так не працює:

type FieldPattern = `/${string}`;
type PathsObject = { [P in FieldPattern]: object; };
const pathObject: PathsObject = 123; // ОК

TypeScript v4.1.2 поводиться так, начебто тип PathsObject є аліасом типу any. Може хтось підкаже, що я роблю не так? Хоча, взагалі то, більше схоже на баг.

Сейчас переписываю проект с использованием TS, может кто подскажет более элегантный способ вместо использования type guard hasObjectKey.
Заранее благодарен.

// Source: https://dev.to/mapleleaf/indexing-objects-in-typescript-1cgi

export function hasObjectKey<T>(obj: T, key: unknown): key is keyof T {
    return (key as string) in obj;
}
import React from 'react';
import classNames from 'classnames';

import { hasObjectKey } from '../utils';

type ColSize = boolean | string | number;

export interface FlexColProps extends React.ComponentPropsWithRef<'div'> {
    className?: string;
    xs?: ColSize;
    sm?: ColSize;
    md?: ColSize;
    lg?: ColSize;
    xl?: ColSize;
    xxl?: ColSize;
}

const BP_SIZES = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const;

const FlexCol = React.forwardRef<HTMLDivElement, FlexColProps>(function FlexCol(
    props,
    forwardedRef
) {
    const { className, ...other }: FlexColProps = props;
    const bpClasses: string[] = [];

    BP_SIZES.forEach((bp) => {
        if (hasObjectKey(other, bp)) {
            const bpVal = other[bp]; 

            if (bpVal) {
                delete other[bp];

                const bpClassBase = (bp as string) === 'xs' ? 'u-flex-col' : `u-flex-col-${bp}`;

                if (bpVal === true) {
                    bpClasses.push(bpClassBase);
                } else {
                    bpClasses.push(`${bpClassBase}-${bpVal}`);
                }
            }
        }
    });

    if (bpClasses.length === 0) {
        bpClasses.push('u-flex-col');
    }

    return <div className={classNames(...bpClasses, className)} ref={forwardedRef} {...other} />;
});

export { FlexCol };

Не актуально, все работает без

hasObjectKey

похоже ранее была опечатка в наименовании переменных.

Нашел более элегантное решение:
stackoverflow.com/...​literal/59187769#59187769

export type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer F)[] ? F : never;
...
type ColSize = boolean | string | number;

const BP_SIZES = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const;

type BpSizes = {
    [K in ElementOf<typeof BP_SIZES>]?: ColSize;
};

export interface FlexColProps extends BpSizes, React.ComponentPropsWithRef<'div'> {
    className?: string;
}
...

Норм статья. Что PHP к джаве плывет, что Javascript. Вот тебе и умирающая джава.

Не джавой единой типизация ограничена. Особенно учитывая текущие обновления TS — совсем не в ту сторону. Много чего из апдейтов вызвано самой природой js и необходимостью все это дело типизировать.

Только оно с якорем плывёт, прицепив за собой мегатонны обратной совместимости. Вместо того чтобы сказать «изя всё, пишите на Жабе»

Начнем с того, что джаве ооочень далеко до того что умеет тайпскрипт в компайл-тайме. Попробуйте написать композицию N функций для любого проиpвольного N с проверкой того что их типы правильным образом накладываются, при это делать все это в компайл тайме и в общем случае. В ТС это делает не очень сложно.

Можешь, плиз, пример привести в TS

Observable.pipe из rx.js, не скажу что прям таки не очень сложно, но реализуемо

Это вы с головы написали? Если есть источники, поделитесь пожалуйста.

В основном, с головы по доке. Источники — conditional types, recursive conditional types variadic parameters и variadic type parameters. Гораздо более простой пример на котором можно потренироватся — реверснуть тьюпл.

Т.е что-то подобное:

type TupleA = [number, string, boolean];
const tupleA: TupleA = [1, "one", true];
const tupleB: ReverseTuple<TupleA> = [true, "one", 1];
Я правильно вас понял?
Что PHP к джаве плывет, что Javascript

а джава то тут каким боком?)

просто джависты любят почесать свое ЧСВ

Спасибо, хороший обзор.

Дякую за статтю.

Використовую TypeScript бо тоді IDE дає якісніші підказки.

Стосовно змін описаних в статті, підкажіть для front-end-у це дійсно так потрібно?

Ну фабрика JSX пригодится точно.

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