В JavaScript есть много ситуаций, когда мы хотим выразить определенные формы объекта на основе условий его атрибутов, например,
// Plain JS - Typical Redux Action types if(action.type === "addUser") { const user = action.user; createUser(user); } if(action.type === "removeUser") { const userId = action.userId; removeUser(userId); }
Вы можете найти этот шаблон во многих других сценариях, таких как представляющий метод запроса ( req.method
-> req.body
), Представляя государство UI ( userReq.isloading
-> userRreq.name
) или даже состояние ошибок ( RESSEST.ERR
-> Результат .msg
). Форма объекта отличается, в зависимости от состояния атрибутов, определенных определенной набором.
В Teadercript мы использовали так называемый дискриминированный тип Union (помеченные союзы), чтобы иметь возможность кодировать форму условного объекта в самом типе. Для нашего предыдущего примера мы бы определили тип для пользователя Действие
так:
// TypeScript type AddUser = { type: "addUser", user: User }; type RemoveUser = { type: "removeUser", userId: string }; type UserAction = AddUser | RemoveUser;
В качестве разработчика Rescript у вас, вероятно, были проблемы сочинительства FFI (Interop) для представления таких помеченных профсоюзов. Как мы можем обрабатывать эти структуры данных без изменения представления JS?
Обычно мы определяем вариант для представления различных форм данных, но, к сожалению, варианты не составляют одинаковой формы пользователей, определенными помеченными профсоюзами.
Эта статья демонстрирует в практическом примере, как мы настроили структуры данных для данных RichTeext (разработанные как меченый союз) для ресер-вариантов.
Важно: .. Мы будем обсуждать только отображение вариантов повторных представлений для неизменных значений JS, поскольку мутации к исходным значениям в конечном итоге не будут отражены в вариантах во время выполнения. Обработка смежных данных требует другая стратегии, которая не покрыта в этом посте.
Фон на корпусе использования
Этот пост основан на реальном использовании, где мне нужно было представлять Storlblok cms ‘ Структуры данных Richtext Data Incript, но не смогли найти подходящую документацию о том, как это сделать.
Я пытался сохранить модель данных простыми, чтобы только захватить основные концепции. Для более тщательной внедренной стороной внедрение модели TS/Rescript Storyblok Richtext, включая логику рендеринга, вы можете проверить Этот репозиторий позже.
Дизайн данных RichText с Teadercript
Чтобы выключить все, мы определим некоторые основные элементы RichText, которые мы хотим иметь возможность представлять: Текст
, Пункт
и Док
. Они будут определены как помеченный союз под названием RichText
:
interface Text { type: "text"; text: string; } interface Paragraph { type: "paragraph"; content: RichText[]; } interface Doc { type: "doc"; content: RichText[]; } export type RichText = | Doc | Text | Paragraph;
Каждый случай RichText
Тип перечисленного выше имеет один общий атрибут Тип
, что помогает системе типа дифференцировать форму заданного значения, проверяя Value.type
, например через Если
или Переключатель
утверждение. Посмотрим, что в действии:
// Recursively iterate through the RichText tree and print all Text.text contents function printTexts(input: RichText) { switch(input.type) { case "doc": case "paragraph": return input.content.forEach(printTexts); case "text": { console.log(input.text); break; } }; } const input: RichText = { type: "doc", content: [ { type: "paragraph", content: [{type: "text", "text": "text 1"}] }, { type: "paragraph", content: [{type: "text", "text": "text 2"}] } ] }; printTexts(input);
Teadercript сможет сделать вывод соответствующих данных для каждого случая правильно большую часть времени Отказ
Есть несколько вещей, которые я лично не люблю в TS при обработке помеченных союзов (особенно через Switch
заявления):
Переключатель
Заявления не являются выражениями (не могут вернуть ценность без упаковки функции вокруг него)- Случаи нуждаются в дополнительных скобках для предотвращения переменной подъемника и необходимости оператора перерыва/возврата для предотвращения падения случаев
- Без каких-либо возвратных заявлений или другой хитрости TS, по-видимому, не делает никаких исчерпывающих проверок в коммутаторах
- Типы различных различных профсоюзов действительно шумны в пространстве типа, и мне часто было трудно навигацию/типы пишений, даже в меньших кодовых базах
- Список выключателей может соответствовать только одно значение сразу. Более сложные дискриминанты/многократные дискриминанты непрактичны
- Типы объектов структурно набираются, а TS не всегда автоматически автоматически выводят тип типа без аннотации типа (как видно в I
Const Invects
Декларация выше). Сообщения об ошибках, как правило, сложнее читать из-за этого.
… Но это все просто мнения.
На следующем шаге давайте рассмотрим, как мы представляем эту модель данных в Rescript.
Представление помеченных профсоюзов в рекрутке
Теперь у нас есть существующее представление RichText, и мы хотим написать код Rescript FFI (Interop), чтобы представлять те же данные без изменения деталей JS.
Система типа Rescript не может выразить помеченные профсоюзы так же, как делает TeampScript, поэтому давайте сделаем шаг назад:
Основная идея помеченных профсоюзов состоит в том, чтобы выразить отношение «A или b или C» и для доступа к разным данным, в зависимости от того, какую ветвь мы в настоящее время обрабатываем. Это именно то, что повторно Варианты сделаны для.
Итак, давайте разработаем предыдущий пример с помощью вариантов. Мы начнем определять модель нашего типа в наших Richtext.res
модуль:
// RichText.res module Text = { type t = {text: string}; }; type t; type case = | Doc(array) | Text(Text.t) | Paragraph(array ) | Unknown(t);
Как вы можете видеть, здесь многое здесь нет. Давайте пройдемся очень быстро:
- Мы определили подмодулю
Текст
, сТип T
представляя текстовый элемент RichText. Мы ссылаемся на этот тип черезText.t
Отказ тип t;
представляет наш фактический меченый союзRichText
элемент. У него нет конкретной формы, которая делает его «абстрактным типом». Мы также назовем этот типRichtext.t
позже.- Наконец, мы определили наши
дело
Вариант, описывающий все разные случаи, как определено с меткой Union в TS. Обратите внимание, как мы также добавилиНеизвестно (T)
Корпус, чтобы иметь возможность представлять собой неправильные/неизвестные элементы Richtext, а также
С помощью этих типов мы можем полностью представлять нашу модель данных, но нам все равно нужно классифицировать входящие данные JS в наши конкретные случаи. Просто для быстрого напоминания: Richtext.t
Тип внутренне представляет объект JS со следующей формой:
{ type: string, content?: ..., // exists if type = "doc" | "paragraph" text?: ..., // exists if type = "text" }
Давайте добавим еще несколько функциональных возможностей, чтобы отразить на этой логике.
Классификация данных Richtext.T
Мы расширим нашу Richtext.res
Модуль со следующими функциями:
// RichText.res module Text = { type t = {text: string}; }; type t; type case = | Doc(array) | Text(Text.t) | Paragraph(array ) | Unknown(t); let getType: t => string = %raw(` function(value) { if(typeof value === "object" && value.type != null) { return value.type; } return "unknown"; }`) let getContent: t => array = %raw(` function(value) { if(typeof value === "object" && value.content != null) { return value.content; } return []; }`) let classify = (v: t): case => switch v->getType { | "doc" => Doc(v->getContent) | "text" => Text(v->Obj.magic) | "paragraph" => Paragraph(v->getContent) | "unknown" | _ => Unknown(v) };
Код выше показывает все, что нам нужно для обработки входящих Richtext.t
значения.
Поскольку мы внутренне управляем объектом JS и необходимый доступ к Тип
и Содержание
Атрибуты, мы определили две небезопасные сырые функции gettype
а также GetContent
Отказ Обе функции получают Richtext.t
Значение для извлечения соответствующего атрибута (при уверенности, что наши данные правильно формируются, иначе мы окажемся с неизвестным
Value).
Теперь с этими двумя функциями на месте мы можем определить классифицировать
Функция для уточнения нашего Richtext.t
в кейс
значения. Это сначала извлекает Тип
из ввода V
и возвращает соответствующий вариант конструктора (с правильной полезной нагрузкой). Так как этот код использует сырой
Функции и полагаются на Obj.magic.
, это считается небезопасным кодом. Для этого конкретного сценария небезопасный код, по крайней мере, выделен в RichText
Модуль (обязательно напишите тесты!).
Примечание: Возможно, вы заметили, что мы храним Содержание
Часть «Док»
объект непосредственно в Док (массив
Вариант конструктор. Поскольку мы знаем, что наша модель DOC не содержит никакой другой информации, мы вместо этого сделали нашу модель проще.
Использование модуля RichText
Теперь с реализацией на месте давайте продемонстрироваем, как мы переживаем на RichText
данные и распечатать каждый Текст
Содержание во всех параграфах:
// MyApp.res // We simulate some JS object coming into our system // ready to be parsed let input: RichText.t = %raw(` { type: "doc", content: [ { type: "paragraph", content: [{type: "text", "text": "text 1"}] }, { type: "paragraph", content: [{type: "text", "text": "text 2"}] } ] }`) // keyword rec means that this function is recursive let rec printTexts = (input: RichText.t) => { switch (RichText.classify(input)) { | Doc(content) | Paragraph(content) => Belt.Array.forEach(content, printTexts) | Text({text}) => Js.log(text) | Unknown(value) => Js.log2("Unknown value found: ", value) }; }; printTexts(input);
Как вы можете видеть в PrintTexts
Функция выше, мы называем функцию RichText.classify
На входном параметре для Док |. Пункт
ветвь Мы можем безопасно унифицировать Содержание
полезная нагрузка (которая оба имеет тип Array
) и рекурсивно называют PrintTexts.
функция снова. В случае Текст
Элемент, мы можем глубоко получить доступ к атрибуту записи Richtext.text.text.text.
и для всех других Неизвестно
корпус, мы напрямую регистрируем ценность
типа Richtext.t
, который является оригинальным объектом JS ( js.log
умеет регистрировать любое значение, независимо от того, какой тип).
В отличие от TS Переключатель
заявление, давайте поговорим о контрольных структурах потока здесь (а именно повторный Switch
утверждение):
- А
Переключатель
это выражение. Последнее утверждение каждого филиала – это возвращаемое значение. Вы даже можете назначить его на привязку (Пусть («Test») {...}
) - Каждая ветвь должна вернуть тот же тип (усиливает более простые проекты)
Самая важная часть в том, что у нас есть полная мощность Схема сопоставления , который можно выполнить на любой структуре повторных данных (цифры, записи, варианты, кортежи, …). Вот только один небольшой пример:
switch (RichText.classify(input)) { | Doc([]) => Js.log("This document is empty") | Doc(content) => Belt.Array.forEach(content, printTexts) | Text({text: "text 1"}) => Js.log("We ignore 'text 1'") | Text({text}) => Js.log("Text we accept: " ++ text) | _ => () /* "Do nothing" */ };
Док ([])
: «Матч на всех элементах дока с 0 элементами в его содержанииДок (контент)
: «Для каждого другого контента (> 0) сделайте следующие …»Текст ({Текст: "Текст 1"})
: «Совпадение всех текстовых элементов, где»Текст ({Text})
: «Для каждого другого текстового элемента с другим текстом сделайте следующие …»_ => ()
: «Для всего остального_
ничего не делай()
»
Расширение модели данных RichText
Всякий раз, когда мы хотим расширить нашу модель данных, мы просто добавляем новый вариант конструктора для нашего дело
Вариант и добавить новый шаблон соответствия в наших классифицировать
функция. Например.
type case = | Doc(array) | Text(Text.t) | Paragraph(array ) | BulletList(array ) // <-- add a constructor here! | Unknown(t); let classify = (v: t): case => switch (v->getType) { | "doc" => Doc(v->getContent) | "text" => Text(v->Obj.magic) | "paragraph" => Paragraph(v->getContent) | "bullet_list" => BulletList(v->getContent) // <-- add a case here! | "unknown" | _ => Unknown(v) };
Это так просто.
Примечание во время выполнения накладных расходов
Стоит отметить, что наши RichText
Модульный подход вводит следующие накладные:
- Варианты с полезными нагрузками представлены в виде массивов, поэтому каждая классификация создаст новый массив с контентом Variant INTER INSER (также дополнительный
Concept
. - Наше
GetContent.
иgettype.
Функция делает дополнительные проверки со структурой каждого входного значения.
Обратите внимание, что команда Compiler Rescript в настоящее время исследует в настоящее время в лучшем представлении времени выполнения для вариантов, чтобы можно было более легко набрать в JS и улучшать производительность в будущем.
Примечание на рекурсии
Я понимаю, что примеры, используемые в этой статье, не являются безопасными стеками. Это означает, что вы можете практически взорвать свой стек вызова, когда достаточно глубоких рекурсивных вызовов. Есть способы оптимизировать примеры, чтобы быть безопасным стеком, просто осознайте, что я пытался сохранить его простым.
Заключение
Мы начали, определив очень простую версию (основанную с историей) Richtext Data Structures в Teadercript и выделены некоторые аспекты различных профсоюзов/помеченных союзов.
Позже мы создали варианты упаковки кода FFI вокруг того же Richtext Data Structures. Мы создали Richtext.res
Модуль определил модель данных с случаи
Вариант и A классифицировать
функция, чтобы иметь возможность проанализировать входящие данные. Мы использовали сопоставление образца для доступа к данным очень эргономичным способом.
Мы только поцарапали поверхность здесь. Я надеюсь, что эта статья дала вам представление о том, как разработать свои собственные модули для рецепта для решения подобных проблем!
Если вы заинтересованы в более реструктурных темах, убедитесь, что Следуй за мной в Twitter Отказ
Особые благодаря Hesxenon и Cristianoc для обширных технических отзывов и обсуждений!
Дальнейшее чтение
- Документация TS: Типы различных профсоюзов
- Моделирование домена с помеченными профсоюзами в GraphQL, RUSIONML и Teadercript Отказ
Оригинал: “https://dev.to/ryyppy/tagged-unions-and-rescript-variants-5e4d”