Казалось бы, бесконечное море руководств по TypeScript, которые вы найдете в Интернете, прекрасно предоставляют ресурсы как начинающим, так и опытным пользователям, но как насчет тех, кто находится посередине? Если вы знаете основы, но еще не достигли продвинутого уровня, то эта серия статей для вас. В нем мы рассмотрим некоторые инструменты и методы, которые поднимут ваш код TypeScript на новый уровень!

В этом руководстве предполагается, что вы уже знакомы с основами TypeScript и JavaScript. Вы должны понимать концепции типов, интерфейсов, ключевого слова as, функций и классов, поскольку мы будем опираться на все эти основы.

Начнем с малого: строительные блоки, которые дает нам TypeScript, и то, что мы можем с ними делать.

Типы объединения (|) и пересечения (&)

Типы объединений и пересечений невероятно полезны для повышения гибкости вашего кода.

Тип объединения, обозначаемый |, позволяет переменной быть одним из нескольких типов. Это полезно, когда мы хотим, чтобы наш код поддерживал различные типы данных. Вот пример использования интерфейсов:

В этом случае person — это либо Teacher, либо Coder. Метод work, общий для Teacher и Coder, действителен, поскольку он присутствует независимо от типа person. Однако методы, специфичные для одного интерфейса, могут не существовать в person, и именно здесь в дело вступает TypeScript, чтобы помешать нам попытаться их использовать. Без дополнительной работы TypeScript позволит нам использовать только те свойства, о существовании которых он знает.

Мы также можем использовать объединения с примитивными типами:

В этом примере мы разрешаем myVar быть числом или строкой. Typescript гарантирует, что мы можем использовать только свойства или методы, общие для всех типов в объединении, что не позволяет нам использовать строковый метод toLowerCase.

Когда мы используем объединение, мы устанавливаем, что любая переменная, присвоенная нашему типу, будет удовлетворять свойствам хотя бы одного из предоставленных типов. Тогда TypeScript пойдет дальше и позволит нам получить доступ только к свойствам и методам, общим для всех типов — по крайней мере, если мы не предоставим TypeScript дополнительную информацию для сужения типа! Не волнуйтесь, если это звучит сложно, вскоре мы углубимся в сужение.

Дальше перекрёсток! Пересечения, обозначаемые &, дополняют объединение и позволяют нам объединить несколько типов в один. Переменная, объявленная с использованием пересечения, должна обладать функциями всех комбинированных типов. Проиллюстрируем это примером:

Как видите, пересечение позволяет нам создать новый тип, объединив перекрывающиеся свойства обоих интерфейсов. Типы пересечений также можно использовать с примитивами, как показано здесь:

Поскольку примитивные типы string и number являются взаимоисключающими, пересечение невозможно. В результате пересечения получается тип never, о котором мы скоро поговорим подробнее. А пока просто знайте, что это символизирует тип, который не может существовать.

Если мы попытаемся пересечь два типа с конфликтующими свойствами, нас также встретит never:

В этом примере пересечения id должно быть одновременно и number, и string, что невозможно. TypeScript видит это и присваивает ему тип never, фактически не позволяя нам использовать только что созданный тип.

В целом, пересечения позволяют нам конструировать сложные типы из простых строительных блоков, а TypeScript гарантирует, что любые значения, которые мы присваиваем пересечению, удовлетворяют всем типам в пересечении.

Эффективное использование объединений и пересечений дает вам возможность легко смешивать и сопоставлять существующие типы, избавляя вас от необходимости создавать похожие типы вручную.

Сужение типа

Одной из наиболее важных функций TypeScript является сужение типа — процесс, при котором тип переменной (подождите) сужается с использованием, например, условного выражения. Это дает TypeScript больше информации о том, какой тип мы ему предоставляем. Давайте расширим предыдущий пример, чтобы увидеть это в действии:

Наша функция specialPrint принимает item, то есть string или number. Затем функция проверит, является ли item string, и в зависимости от результата будет использовать либо toLowerCase, либо toFixed. Благодаря условным выражениям, в которых содержатся методы, Typescript распознает, что код может достичь этих методов только в том случае, если item является правильным типом, и обойдет свои ограничения на методы, которые не являются общими для всех типов в объединении.

У вас когда-нибудь была переменная в подвешенном состоянии, ожидающая ввода пользователя или завершения сетевого запроса? Эти переменные обычно начинаются с undefined, что может быть частым источником головной боли для многих разработчиков JavaScript. TypeScript дает нам инструменты для решения этой проблемы, используя комбинацию объединений и сужения типов. Такой подход помогает нам писать чистый и безошибочный код!

В этом примере любой Person может быть Teacher, Coder или null. Поскольку у null нет методов, мы не можем напрямую вызвать person.work(). Вместо этого мы должны убедиться, что person не является null с простым условием. Как только мы это сделаем, TypeScript распознает, что person имеет тип Teacher | Coder, и мы сможем безопасно вызвать метод work.

Вот где действительно проявляется сила сужения типов. Используя проверки типов, мы можем гарантировать, что наш код ведет себя должным образом и не содержит ошибок во время выполнения. Это делает наш код более безопасным, простым для понимания и более простым в обслуживании. Есть еще много способов использовать сужение типов в Typescript, но это статья для другого дня. А пока просто знайте, что TypeScript следит за вашими условными операторами и понимает, какие типы в них возможны.

Любой, неизвестный и никогда

TypeScript имеет несколько специальных типов: any, unknown и never. Начнем с рассмотрения any:

Благодаря any приведенный выше код вполне приемлем и не выдаст никаких ошибок! Переменной типа any можно присвоить любое значение, которое вы хотите, и другие переменные разных типов могут быть без проблем присвоены ей. Другими словами, TypeScript не выполняет проверку типа переменной типа any. Хотя иногда вы можете захотеть, чтобы переменная обрабатывалась подобным образом, проверка типов — это одна из лучших причин использовать TypeScript в первую очередь. Это означает, что вместо этого обычно лучше использовать unknown.

Хотя вы можете установить для переменной unknown любое значение, какое захотите, другие переменные не могут быть присвоены ей напрямую. Вместо этого вы должны явно указать его тип, например:

Использование unknown предпочтительнее использования any, поскольку оно побуждает нас явно выбирать, когда и как мы преобразуем его в другой тип.

Наконец, давайте посмотрим на never:

Тип never является ограничительным, поскольку он запрещает присвоение ему каких-либо значений. Аналогично, никакие другие переменные не могут быть установлены для переменной типа never. Этот тип символизирует невозможное; то, что никогда не должно произойти. Обычное место, где мы можем увидеть never, — это функции, которые всегда должны выдавать ошибки и никогда возвращать значение:

Я составил краткую сводную диаграмму для этих трех типов:

Все три из них служат разным целям, каждый из которых имеет разный баланс между безопасностью типов и гибкостью. Хотя any обеспечивает максимальную гибкость, за это приходится терять преимущества безопасности типов, предоставляемые TypeScript. По этой причине вместо этого вам следует использовать тип unknown и явно указать TypeScript на приведение ваших переменных. Наконец, тип never всегда выдает нам ошибку, когда мы пытаемся с ним взаимодействовать. Мы используем never, когда хотим сообщить TypeScript, что не должно быть сценария, в котором может возникнуть результат, и что вы хотите, чтобы в случае его возникновения выдавалась ошибка. В будущих статьях мы углубимся в некоторые расширенные варианты использования never.

Литеральные типы

TypeScript также предоставляет нам литералы — еще одну мощную функцию, которая представляет точные значения и может быть конкретным экземпляром string, number или boolean. Давайте посмотрим на один:

Здесь мы создали переменную userRole с типом "basic". По этой причине переменную myUserRole больше нельзя присвоить какой-либо другой строке. Литерал сам по себе не кажется таким уж полезным, но в сочетании с некоторыми другими функциями, которые мы изучили, мы можем создать несколько полезных типов:

Комбинируя объединения и литералы, мы создали тип, который может иметь одно из трех возможных значений. Это может пригодиться!

Когда мы создаем переменную с let или var, TypeScript обычно присваивает ей общий тип (например, string, number или boolean). Альтернативно, если мы объявим переменную с const, TypeScript предположит, что ее тип является литералом. Давайте посмотрим на пример:

Мы также можем указать Typescript вести себя таким образом при объявлении с помощью let или var, используя as const.

as const — это полезный способ убедиться, что ваши типы являются максимально конкретными. Кроме того, мы можем использовать as const для объектов, чтобы гарантировать конкретность полей:

В целом, литералы — отличный способ ограничить значения, которые могут содержать переменные. Вы можете видеть, что as const изменил тип полей нашего объекта на литералы. Однако также добавлено ключевое слово readonly. Что тут происходит?

Только чтение

TypeScript представляет несколько новых ключевых слов, одно из которых — readonly. Это ключевое слово позволяет нам указать части объекта как практически неизменяемые. Давайте расследуем:

Теперь TypeScript будет выдавать нам ошибку всякий раз, когда мы пытаемся изменить (или «записать») переменную id, отсюда и название readonly. Свойство readonly property можно установить только во время инициализации или при создании объекта. Эта концепция также применима к классам:

Как и в нашем примере интерфейса, мы не можем изменить поле id после его инициализации. Для класса это означает, что мы можем изменить только id в конструкторе. После завершения выполнения конструктора TypeScript больше не позволит нам изменять это поле.

Наконец, readonly можно применить к массивам:

Это не позволяет нам добавлять, удалять или изменять какой-либо элемент в нашем массиве, но мы все равно можем назначить новый массив содержащей его переменной.

readonly — отличное ключевое слово для использования в больших базах кода, поскольку оно позволяет явно выбирать, когда что-то должно быть наблюдаемым, но неизменяемым. Это запрещает людям случайно изменять данные, которые они не должны иметь, что, в свою очередь, предотвращает ошибки и проясняет цель вашего кода.

Заключение

Функции, описанные в этой статье, являются одними из наиболее часто используемых в TypeScript, и, вдумчиво используя их самостоятельно, вы можете создавать менее подверженные ошибкам системы и сохранять присущую JavaScript гибкость. Помните, что TypeScript не усложняет вам жизнь! Речь идет о том, чтобы сделать ваш код более безопасным, удобным в сопровождении и простым для понимания.

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