Казалось бы, бесконечное море руководств по 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
является ограничительным, поскольку он запрещает присвоение ему каких-либо значений. Аналогично, никакие другие переменные не могут быть установлены для переменной типа neve
r. Этот тип символизирует невозможное; то, что никогда не должно произойти. Обычное место, где мы можем увидеть 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. Следите за обновлениями!