Начиная с версии 0.51 Flow добавлена поддержка непрозрачных типов. Если вы ищете официальный пост в блоге, вы можете найти его здесь. Сегодня мы рассмотрим простой пример того, как мы можем использовать непрозрачные типы в нашем коде. Начнем с простого фрагмента из нашего приложения:
// index.js function sendEmail(address: string, message: string): void {}
В этом базовом объявлении четко указано, что наша функция sendEmail принимает адрес электронной почты в виде строки, что предотвращает очевидные ошибки, такие как передача числа или объекта вместо строки:
// OK sendEmail("[email protected]", "Hello, world!"); // Error! sendEmail(124, "Hello, world!");
Хотя этот уровень объявлений уже полезен, он по-прежнему позволяет нам передавать явно неправильные значения в качестве адреса электронной почты:
// No Flow errors, but obviously wrong sendEmail("just-some-random-string", "Hello, world!");
Что мы можем сделать, чтобы избежать подобных ошибок? Конечно, мы можем объявить класс Email и использовать его как тип аргумента. Несмотря на правильность, основанный на классах подход вынуждает нас объявлять ненужные классы и создавать экземпляры объектов этих классов во время выполнения. Непрозрачные типы — это более элегантный подход, который мы можем использовать без каких-либо затрат времени выполнения нашего приложения.
Давайте посмотрим, как мы можем объявить наш тип Email, используя объявление непрозрачного типа (важно переместить объявление типа в отдельный файл, так как непрозрачный тип непрозрачен для всего кода за пределами JS модуль, в котором он был объявлен, в то время как он продолжает работать как псевдоним общего типа внутри модуля, в котором он был объявлен):
// Email.js export opaque type Email = string; const EMAIL_REGEXP = /.../; export function emailOfString(value: string): Email { if (EMAIL_REGEXP.test(value)) return value; throw new Error("Wrong email format"); } export function stringOfEmail(email: Email): string { return email; }
Вот и все. Теперь каждая часть вашего кода за пределами Email.js не может создавать значения типа электронной почты или использовать строки в качестве значений электронной почты. Таким образом, каждая функция, которая объявляет свой тип ввода как Email, теперь может с уверенностью предположить, что это правильный (или, по крайней мере, проверенный) адрес электронной почты, а не какая-то произвольная строка, и избежать дополнительных проверок во время выполнения. Давайте посмотрим, как теперь выглядит наш index.js:
// index.js import type {Email} from "./Email"; import {emailOfString} from "./Email"; function sendMail(address: Email, message: string): void {} // OK! sendEmail(emailOfString("[email protected]"), "Hello, world!"); // Error! still an error, as earlier sendEmail(124, "Hello, world!"); // Error! string can not be used as Email sendEmail("[email protected]", "Hello, world!");
Так как любое взаимодействие со значениями типа Email возможно только через общедоступный интерфейс модуля Email.js, мы можем безопасно изменить внутреннее представление типа Email с небольшим риском поломки изменений:
// Email.js (alternative internal representation) export opaque type Email = {address: string}; const EMAIL_REGEXP = /.../; export function emailOfString(value: string): Email { if (EMAIL_REGEXP.test(value)) return {address: value}; throw new Error("Wrong email format"); } export function stringOfEmail(email: Email): string { return email.address; }
По-прежнему существует некоторый риск поломки изменений, если мы будем использовать представление с небезопасным типом, если мы будем использовать некоторые небезопасные операции, такие как JSON.stringify. Используя альтернативную реализацию электронной почты, мы изменим вывод сериализованных строк без ошибок типа, чтобы предотвратить это.
В некоторых случаях явное преобразование типов из непрозрачных типов, в целом очень безопасное, может стать утомительным, особенно для числовых непрозрачных типов (мы не можем использовать их в арифметических операциях без явных преобразований):
// PositiveNumber.js export opaque type PositiveNumber = number; // some-other-file.js function add(x: PositiveNumber, y: PositiveNumber) { x + y; // ERROR! }
Чтобы упростить себе жизнь, мы можем разрешить использовать значения непрозрачного типа в качестве их более общего типа:
// PositiveNumber.js export opaque type PositiveNumber: number = number; // some-other-file.js function add(x: PositiveNumber, y: PositiveNumber) { x + y; // OK! }
Теперь мы можем свободно использовать наши значения типа PositiveNumber в качестве чисел. Но, что важно, это преобразование одностороннее — мы можем использовать значения PositiveNumber как числа, но не иначе. Это очень важно, потому что у нас все еще есть гарантии, что произвольные числа не могут быть использованы в качестве значений PositiveNumber — для этого потребуется явное преобразование в тип PositiveNumber, но нам не нужно делать явное out типа PositiveNumber, что приводит к более компактному коду.
Примечание — вы можете использовать непрозрачные типы не только для простых значений, таких как строки и числа, но и для типов объектов, вы можете сделать их универсальными, используя переменные типа и т. д.
Это был первый небольшой пост в будущих сериях о практических примерах использования Flowtype для того, чтобы сделать Javascript-приложения более безопасными и корректными. Не стесняйтесь задавать вопросы и рассказывать мне, какие еще темы о Flow вас интересуют. Я здесь не эксперт, но я использую Flow в течение нескольких лет для довольно большой кодовой базы и выбрал несколько полезных приемов.