Я работаю с TypeScript уже не первый год и считаю, что он достаточно прост, особенно для людей с опытом разработки на Java. Тем не менее, прочитав новости о последнем крупном обновлении TypeScript 4.1, я сильно удивилась тому, как много я еще о нем не знаю.
Не думаю, что такое неведение присуще мне одной. Эти новости подтолкнули меня к более глубокому изучению функционирования системы типов, и теперь я хочу рассказать вам о новых прекрасных возможностях и изменениях версии 4.1. В ходе этого процесса я дам пояснения ключевых слов и приведу много наглядных примеров.
Если вы уже уверенно освоили основы этого языка и хотите узнать о его продвинутых возможностях, то данный материал окажется весьма кстати.
Новые возможности
Шаблонные литералы
Этот функционал появился в ES6 и подразумевает использование при работе со строками не одинарных и двойных кавычек, а обратных:
const message = `text`;
Как пояснил Флавио Коупс, строковые литералы предоставляют множество возможностей, которых не имеют стандартные строки:
- улучшенный синтаксис для определения многострочных строк;
- легкий способ интерполяции переменных и выражений в строки;
- создание DSL (предметно-ориентированных языков) с помощью тегов шаблонов.
Синтаксис типов шаблонных литералов такой же, как и у строковых литералов в JS, только задействуются они в позиции типа:
type Entity = 'Invoice';
type Notification = `${Entity} saved`;
// эквивалент
// type Notification = 'Invoice saved';
type Viewport = 'md' | 'xs';
type Device = 'mobile' | 'desktop';
type Screen = `${Viewport | Device} screen`;
// эквивалент
// type Screen = 'md screen' | 'xs screen' | 'mobile screen' | 'desktop screen';
При использовании с конкретными типами литералов новый тип строкового литерала создается конкатенированием содержимого.
Пересопоставление ключей в отображаемых типах
Отображаемый тип может создавать новые типы объектов на основе произвольных ключей. Строковые литералы в таких типах могут задействоваться в качестве имен свойств.
Отображенные типы TypeScript:
type Actions = { [K in 'showEdit' | 'showCopy' | 'showDelete']?: boolean; };
// эквивалент
type Actions = {
showEdit?: boolean,
showCopy?: boolean,
showDelete?: boolean
};
Если вас интересует возможность создания новых ключей или их фильтрации, то TS 4.1 позволяет пересопоставлять ключи в отображаемых типах с помощью спецификатора as
.
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: 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>;
// ^ = type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
// Удаляем свойство 'kind'
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^ = type KindlessCircle = {
// radius: number;
// }
Фабрики JSX
JSX означает JavaScript XML. Он позволяет писать HTML-элементы в JS и помещать их в DOM без участия методов createElement()
и/или appendChild()
.
Пример фабричных функций React JSX:
const greeting = <h4>Yes I can do it!</h4>;
ReactDOM.render(greeting, document.getElementById('root'));
TS 4.1 поддерживает фабричные функции jsx
и jsxs
из React 17 с помощью двух новых опций параметра компилятора jsx
:
react-jsx
react-jsxdev
“Эти опции предназначены для продакшен-компиляции и компиляции в среде разработке, соответственно. При этом часто бывает, что одна расширяет другую” — из заметок TypeScript.
Вот два примера из документации, описывающие конфигурацию для продакшена и разработки:
tsconfig.json: пример конфигурации TS для продакшен-сборок:
// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": ["./**/*"]
}
tsconfig.dev.json: пример конфигурации TS для сборок среды разработки:
// ./src/tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}
Рекурсивные условные типы
Еще одно новшество релиза, делающее обработку условных типов более гибкой, позволяя им ссылаться на самих себя внутри своих ответвлений. Вот пример разворачивания глубоко вложенного промиса при помощи Awaited
:
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>>;
Однако стоит отметить, что на проверку рекурсивных типов TS требуется больше времени. Microsoft рекомендуют использовать их умеренно и продуманно.
Проверка индексных обращений
Сигнатуры индексов в TypeScriptпозволяют обращаться к свойствам с произвольным именем, как показано в интерфейсе Options
ниже. Здесь мы видим, что свойство, в котором не указан path
или permissions
, должно иметь тип string | number
.
Проверка индексных обращений (источник):
interface Options {
path: string;
permissions: number;
// Эта сигнатура индекса получает дополнительные свойства.
[propName: string]: string | number;
}
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// Все эти инструкции тоже допустимы
// и имеют тип 'string | number'.
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}
Новый флаг --noUncheckedIndexedAccess
активирует режим, в котором любое обращение к свойству (например, opts.path
) или индексное обращение (например, opts["blabla"]
) трактуется как потенциально undefined. Это означает, что если вам нужно, как в данном примере, обратиться к свойству opts.path
, то сначала необходимо выполнить проверку его наличия или использовать оператор ненулевого утверждения (постфикс !
).
Применение флага --noUncheckedIndexedAccess
(источник):
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
// При использовании noUncheckedIndexedAccess следующие действия //недопустимы
opts.yadda.toString();
Object is possibly 'undefined'.
opts["foo bar baz"].toString();
Object is possibly 'undefined'.
opts[Math.random()].toString();
Object is possibly 'undefined'.
// Проверка присутствия свойства
if (opts.yadda) {
console.log(opts.yadda.toString());
}
// Утверждение в стиле "Я знаю, что делаю"
// с помощью оператора '!'
opts.yadda!.toString();
}
Флаг --noUncheckedIndexedAccess
помогает перехватывать многие ошибки, но может внести лишний шум в большой объем кода, в связи с чем не активируется флагом --strict
автоматически.
Paths без baseUrl
До выхода TS 4.1 для использования paths
в файле tsconfig.json
приходилось объявлять параметр baseUrl
. В новом же релизе допускается определение paths
без этого параметра. Таким образом решается проблема неправильных путей при автоматических импортах.
Пути и baseUrl
в tsconfig.json:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@shared": ["@shared/"] // Это сопоставление относится к //"baseUrl"
}
}
}
checkJs
теперь подразумевает allowJs
Если у вас есть JS-проект, где вы используете опцию cjeckJs
для регистрации ошибок в файлах .js, то вы также должны были объявить allowJs
, чтобы JavaScript мог эти файлы компилировать. В TS 4.1 такой необходимости больше нет, поскольку теперь checkJs
подразумевает allowJs
по умолчанию.
checkJs
и allowJs
в tsconfig.json:
{
"compilerOptions": {
"allowJs": true,
"checkJs": true
}
}
Поддержка редакторами JSDoc-тега @see
Теперь при работе с TS в редакторах реализована улучшенная поддержка тега @see
.
Применение тега @see
(источник):
// @filename: first.ts
export class C {}
// @filename: main.ts
import * as first from "./first";
/**
* @see first.C
*/
function related() {}
Кардинальные изменения
Изменения lib.d.ts
lib.d.ts
— это файл, создаваемый при каждой установке TypeScript. Он содержит внешние объявления для разных стандартных JS-конструкций, присутствующих в среде выполнения JavaScript и DOM, упрощая написание JS-кода с проверкой типов.
Этот файл автоматически включается в контекст компиляции проекта TS. Его можно исключить, указав в tsconfig.json
флаг компилятора --nolib
или "noLib": true
.
В связи со способом автоматической генерации типов DOM в TS 4.1, в lib.d.ts
изменился ряд API. Например, Reflect.enumerate
был удален, в том числе из ES2016.
"abstract"
членов нельзя отмечать как «async"
Теперь членов, отмеченных как abstract
, нельзя отмечать как async
. Так что для коррекции кода вам придется удалить ключевое слово async
.
Абстрактный класс TypeScript:
abstract class MyClass {
abstract async create(): Promise<string>;
}
any
/unknown
распространяются на ложные позиции
До версии 4.1 в выражениях, наподобие foo && somethingElse
, типом foo
был либо any
либо unknown
. Типом всего выражения был бы тип somethingElse
, который в следующем примере определен как { someProp: string }
:
declare let foo: unknown;
declare let somethingElse: { someProp: string };let x = foo && somethingElse;
В TS 4.1 any
и unknown
распространяются не на тип в правой части, а во вне. Обычно исправить это можно сменой foo && someExpression
на !!foo && someExpression
.
Внимание: двойной восклицательный знак (!!
) является сокращенным способом приведения переменной к логическому значению (true или false).
Параметры для resolve в промисах теперь обязательны
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()
требуется произвести без аргумента.
Промис с общим аргументом void
:
new Promise<void>((resolve) => {
doSomethingAsync(() => {
doSomething();
resolve();
});
});
Условные операторы распространения создают необязательные свойства
В JavaScript распространения объектов { ...files}
не работают для ложных значений. Это подразумевает пропуск files
в случае, когда он null
или undefined
.
Если в следующем примере использования условного распространения объект files
определен, то свойства file.owner
в него распространятся. В противном случае никакие свойства в возвращаемый объект не передаются:
function getOwner(file?: File) {
return {
...file?.owner,
defaultUserId: 123,
};
}
До выхода TypeScript 4.1, getOwner
на основе каждого распространения возвращал тип-объединение:
{ x: number } | { x: number, name: string, age: number, location: string }
- Если
file
определен, то передаются все свойства изPerson
(типowner
). - В противном случае ни одно.
Тем не менее это ведет к серьезному расходу ресурсов и обычно преимуществ не дает. Например, при сотнях распространений в одном объекте, каждое из которых потенциально добавляет сотни или тысячи свойств. Поэтому для повышения производительности в TypeScript 4.1 возвращаемый тип иногда использует все свойства в необязательной форме:
{
x: number;
name?: string;
age?: number;
location?: string;
}
Несопоставимые параметры больше не связываются
Параметры, которые не соответствуют друг другу, ранее соотносились друг с другом через тип any
.
В следующем примере перегрузки (передачи в одну функцию нескольких типов) функция pickCard
будет возвращать один из двух вариантов в зависимости от переданных пользователем данных. Если он передаст объект, представляющий набор карт, то функция вернет случайную карту из этого набора. В случае же выбора карты по ее номеру пользователем, в ответ будет возвращаться эта карта.
Перегрузки JavaScript (источник):
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)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
В TypeScript 4.1 некоторые случаи присваивания могут не работать, как и некоторые перегруженные функции. В качестве обходного пути рекомендуется использовать утверждение типов.
Заключение
TypeScript экономит ваше время, перехватывая ошибки и обозначая их до выполнения кода. Углубившись в понимание принципов его работы, вы будете легче находить способы оптимизации структуры приложения и реализации сложных сценариев. Надеюсь, эта статья помогла вам лучше понять систему типов TypeScript, что позволит более эффективно применять этот язык в дальнейшем.
TypeScript 4.1 доступен через NuGet или NPM:
npm install typescript
Читайте также:
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Rakia Ben Sassi: What Are Template Literal Types in TypeScript 4.1?