Начиная с версии 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 в течение нескольких лет для довольно большой кодовой базы и выбрал несколько полезных приемов.