Как мы создали надежную служебную функцию, используя мощные возможности TypeScript

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

Как появилась функция полезности

Если бы вам пришлось написать логику, чтобы проверить, определен ли объект определенного типа и действительно ли у объекта есть какие-либо ключи, простой способ сделать это в TypeScript:

Если бы вам приходилось использовать эту логику неоднократно, в идеале было бы создать функцию, которая инкапсулирует эту логику. Написанная на TypeScript, эта функция будет выглядеть примерно так:

Хорошая штука, правда? Давайте воспользуемся этой функцией полезности и посмотрим, что произойдет. Приведенный ниже фрагмент кода показывает, что мы видим в редакторе кода VS, когда используем служебную функцию.

Хорошо, что только что произошло? Почему у нас ошибка выше?

Если вы внимательно посмотрите на приведенный выше фрагмент кода, то внутри функции будет та же логика, которую мы использовали до написания функции, но по какой-то причине TypeScript жалуется. Что мы делаем не так?

Проблема I. TypeScript не «распознает» логику служебной функции

Как решить эту проблему, если TypeScript не распознает правильную логику, содержащуюся в служебной функции isPresentSomeObject?

Один из способов исправить это - использовать ненулевой оператор утверждения (!) В TypeScript, который, по сути, означает, что вы сообщаете компилятору, что вы знаете, что делаете.

Хотя это работает, я не большой поклонник этого подхода, возможно, потому, что я ленив и не хочу вводить ненулевой оператор утверждения (!) Каждый раз, когда мне нужно использовать эту функцию.

Но серьезно, есть ли лучший, более масштабируемый способ переписать это? Задержитесь на этой мысли, пока мы рассмотрим, в чем состоит вторая проблема с этой функцией.

Проблема II: служебная функция в настоящее время не поддерживает другие типы

Наша служебная функция работает с аргументами типа SomeObjectType . Что, если бы нам пришлось использовать ту же функцию с другим объектом другого типа, AnotherObjectType? Мы могли бы реорганизовать служебную функцию для поддержки нового типа, создав объединение существующего типа и нового типа, как показано ниже:

Хорошо, допустим, мы должны поддерживать третий тип и четвертый тип? Добавить типы в союз? Шутки в сторону? Что, если бы был пятый тип?

Вы согласитесь, что создание объединения большего количества типов не масштабируется, и на этом этапе вы можете поймать себя на мысли: «Почему бы просто не сделать аргумент типа any, чтобы он мог принимать все 'типы?

Эээ, подожди! Проблема с использованием типа any заключается в том, что мы теряем безопасность типов, которая обеспечивается TypeScript. Итак, как мы можем переписать эту функцию без использования типа any?

Привет, Generics!

Согласно Википедии, Generics - это средство универсального программирования, которое позволяет вам расширять систему типов, чтобы тип или метод мог работать с объектами различных типов, обеспечивая безопасность типов во время компиляции.

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

Надеюсь, нет, потому что приведенный выше фрагмент кода нарушает принцип СУХОЙ. Вместо этого нам нужен способ повторно использовать эту логику независимо от типа аргумента. Уже замечаете сходство между этой функцией и нашей функцией полезности?

Generics предоставляет ту абстракцию, которая позволяет вам определять типы параметров (которые будут указаны позже), что позволяет повторно использовать код с различными типами. С дженериками приведенная выше функция идентификации становится:

Делая это, мы сообщаем компилятору TypeScript: «эй, тип аргумента имеет тип параметра, T, и вы узнаете, что такое T во время компиляции ». Это позволяет нам передавать любой тип в качестве параметра без потери проверки типов.

Переписав нашу служебную функцию для использования универсальных шаблонов, функция станет следующей:

Благодаря этому вы получаете и динамизм, связанный с использованием типа any, и безопасность, обеспечиваемую проверкой типов: двойной выигрыш, если вы спросите меня.

Задача 1 решена! Следующая остановка, как заставить компилятор «распознавать» нашу служебную функцию?

Введение в охрану определяемого пользователем типа

Если вы какое-то время использовали TypeScript, но не слышали о типах защиты, вы попали в хорошую компанию. Пока мы не столкнулись с этой проблемой, я о них тоже не слышал.

Представим, что у вас есть следующие определенные интерфейсы:

Если бы вам пришлось реализовать логику, которая проверяла бы, к какому типу относится animal, как бы вы это сделали? Один из способов - использовать утверждение типа:

Большой! Что произойдет, если к объединению типов Animal будет добавлено больше типов?

type Animal = Cat | Dog | Goat | Horse | Kangaroo | ...

Написание функции для каждого нового типа не масштабируется. Как же тогда мы можем создать служебную функцию, которая позволяет компилятору TypeScript определять тип переменной без явного использования утверждения типа? Вот где в игру вступают типажи.

Из документации TypeScript,

Защита типа - это некоторое выражение, которое выполняет проверку во время выполнения, которая гарантирует тип в некоторой области.

Что это вообще значит?

Суть в том, что с помощью защиты типов вы можете указать компилятору TypeScript вывести определенный тип для переменной в определенном контексте.

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

Предикаты - это функции, возвращающие логические значения. Предикаты типа имеют форму parameterName is Type , parameterName - это имя аргумента функции и Type тип аргумента, и они также возвращают логические значения.

Как вы определяете типовой охранник?

Чтобы определить защиту типа, просто определите функцию, возвращаемый тип которой является предикатом типа.

Таким образом, защита типа isCat - это функция, которая имеет предикат animal is Cat в качестве возвращаемого типа, как показано ниже:

Обратите внимание, что мне даже не пришлось реализовывать какую-либо «серьезную» логику в функции, все, что мне нужно было сделать, это вернуть true, потому что помните, что предикаты - это функции, возвращающие логические значения.

Защита типа isCat сообщает системе типов, что мы подтвердили, что переданный аргумент animal является Cat, поэтому, когда компилятор выполняет анализ потока управления и видит нашу защиту типа, он говорит: О! Все, что передается в эту функцию, должно быть кошкой, если она возвращает true .

Затем он сужает тип аргумента с Animal до Cat,. Таким образом, мы гарантируем, что тип, который мы получим во время выполнения, будет структурой, согласованной с Cat определением интерфейса.

Чтобы еще больше оценить охранников типов, давайте используем их в блоке if-else:

Еще раз вспомните, что мы не реализовали «серьезную» логику в isCat type guard, но компилятор видит возвращаемый тип как Cat из-за предиката animal is Cat; поэтому он не вызывает ошибок в блоке if приведенного выше фрагмента кода.

Однако более интересно то, что происходит в блоке else.

Поскольку компилятор ожидает тип Cat в if block, он говорит, что все, что находится в else block, должно быть другого типа - этим типом является тип never, который используется для значений, которые никогда не встречаются, отсюда и сообщение об ошибке, которое вы см. в else block.

Надеюсь, это было ясно, теперь давайте применим защиту типов к нашей служебной функции и посмотрим, как это решает проблему, описанную в первом испытании.

Преобразование нашей служебной функции в охранник типа

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

Однако предикат какого типа мы используем для этой служебной функции?

Напомним, что универсальные шаблоны позволяют вам определять типы параметров, которые затем назначаются во время компиляции. Чтобы преобразовать нашу служебную функцию в защиту типа, мы могли бы воспользоваться этим и сделать наш возвращаемый тип предикатом типа в форме arg is T, где T - это тип аргумента.

И теперь это работает!

Заключение

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

Если вы хотите узнать больше о защите типов, системе типов или компиляторе в целом, вы можете найти ссылки в разделе «Дополнительная литература».

Если вы хотите поговорить об этом или о чем-нибудь вообще, я с радостью подключусь через Twitter или LinkedIn.

Оставайтесь в безопасности 😊.

TL;DR:

  • Если вы поймали себя на том, что используете тип any как способ сделать часть кода более надежной, подумайте о дженериках.
  • Защиты типов являются мощным средством для сужения типов, удовлетворяя процесс потока управления компилятора TypeScript и гарантируя безопасность типов во время выполнения.
  • Подумайте о том, что тип охраняет, когда тип, который вы хотите использовать, не так конкретен, как вам хотелось бы, или когда вы хотите убедиться, что внешние данные относятся к определенному типу.
  • Существуют встроенные средства защиты типов, которые поставляются с TypeScript, такие как typeof и instanceof, но средства защиты типов, определяемые пользователем, могут быть более мощными при правильном использовании.
  • Как и все другие мощные средства защиты, нельзя злоупотреблять защитой пользовательского типа; их не следует рассматривать как способ взломать компилятор TS.

Дальнейшее чтение