TypeScript - отличный язык. TypeScript берет JavaScript и делает его действительно хорошим. Если есть один вопиющий недостаток, это невозможность использовать строго типизированные блоки catch. Однако в основном это связано с недостатком дизайна в языке JavaScript; в JavaScript можно throw что угодно, а не только Error типы.

Обоснование

Рассмотрим следующий полностью допустимый код TypeScript:

Легко понять, почему это может быть проблемой. Один вариант использования, где это не идеально, - это использование веб-API от TypeScript. Часто неуспешные коды состояния HTTP (500 Internal Server Error, 404 Not Found и т. Д.) Выдаются как ошибка кодом потребителя API.

использование

Рассмотрим пример использования некоторых утилит из AndcultureCode.JavaScript.Core и AndcultureCode.JavaScript.React, которые завершают ответы и ошибки API в типах ResultRecord и ResultErrorRecord Immutable.js Record.

В этом примере вы можете видеть, что обработка ошибок изначально в TypeScript… довольно небрежна. Общий шаблон возможно, монада для более общей обработки ошибок и потока управления. По сути, то, что мы хотим сделать, - это создать абстракцию, которая может строго типизировать выброшенные ошибки для указанного типа, который, как вы знаете, вероятно, будет выдан. В нашем случае мы хотим иметь возможность обрабатывать ошибки строго типизированного ResultRecord с ResultErrorRecord внутри него.

Что, если бы мы могли взять пример выше и представить ту же логику, но с меньшим количеством кода и строгой типизацией в блоке catch? В следующем примере одно из result или error будет ненулевым, но не оба.

Этот шаблон дает нам более функциональный подход к обработке ошибок, дает нам строго типизированные ошибки и действительно очень хорошо работает в сочетании с ловушками React. Давайте посмотрим на простой пример React с использованием некоторой инфраструктуры из AndcultureCode.JavaScript.Core, AndcultureCode.JavaScript.React и AndcultureCode.JavaScript.React.Components:

Чистая, лаконичная и строго типизированная обработка ошибок всего в 46 строках кода, включая пользовательский интерфейс.

Выполнение

Так как же эта навороченная Do.try работа под капотом? Добавив абстракцию поверх обычных старых Promises. Давайте разберемся с этим.

Во-первых, давайте определим некоторые типы утилит, которые нам понадобятся:

Теперь посмотрим на наш конструктор:

Это private constructor не ошибка. Как вы заметили в предыдущих фрагментах, использование этого шаблона начинается с Do.try; это потому, что try - это статический фабричный метод, который возвращает экземпляр Do. private constructor может вызываться только внутри класса методом try. Реализация try очень проста:

Метод finally столь же прост, но с одной важной оговоркой:

Обратите внимание на возвращаемое значение return this;. Это позволяет использовать цепочку методов, т.е. Do.try(workload).catch(catchHandler).finally(finallyHandler);. В этом коде catch и finally вызываются в одном и том же экземпляре Do, который возвращается из Do.try.

Также существует getAwaiter метод, который позволяет нам await получить результат. Все, что нам нужно сделать, это вернуть внутреннее обещание.

Теперь перейдем к самому интересному. catch метод. Внутри метода catch мы будем набирать охрану брошенного объекта; если брошенный объект является экземпляром ResultRecord, мы приводим его как таковой и передаем в качестве первого аргумента обработчика catch; в противном случае это какая-то неизвестная ошибка, поэтому мы передаем ее как второй аргумент обработчика catch. Нам также нужно вернуть обещание к Promise<TReturnVal> из-за типа возвращаемого значения Promise.catch, но обещание по-прежнему является действительным Promise<TReturnVal>.

И вот у вас есть базовая реализация «может быть монады». Хотя реализация здесь является самоуверенной, предлагая строго типизированную обработку ошибок для ResultRecord ошибок, вы можете легко реализовать то же самое практически для любого типа, который вы хотите использовать для завершения своих ошибок, при условии, что вы можете реализовать типа охранник для него.

Продолжая

Я думаю, что строго типизированная обработка ошибок говорит сама за себя, но мы можем пойти еще дальше. Этот шаблон позволяет получить чрезвычайно мощную утилиту, и я думаю, что это самый веский аргумент в пользу ее использования: поведение по умолчанию. Мы можем расширить наш класс Do, чтобы иметь глобальную конфигурацию, что позволяет нам определять поведение по умолчанию, которое применяется к каждому экземпляру Do во всем приложении.

Все, что нам нужно сделать, это добавить механизм статической конфигурации и реализовать проверку нашей конфигурации внутри конструктора:

Итак, как выглядит применение поведения по умолчанию? Приведем пример.

Мы работаем над крупномасштабным приложением React, и, чтобы облегчить отладку ошибок во время разработки, мы хотим всегда записывать ошибки в консоль в среде разработки. Что ж, с только что добавленным механизмом конфигурации добавить это поведение по умолчанию становится тривиально просто. Просто откройте свою index.ts точку входа в приложение и добавьте обработчик:

Вы можете использовать тот же механизм конфигурации, чтобы добавить поведение по умолчанию к try или finally частям цепочки вызовов. Не стесняйтесь ознакомиться с полной реализацией, использованной в производстве, здесь.

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

Что вы думаете? Собираетесь ли вы попробовать «возможно, монады» или шаблон Do.try в своем следующем проекте TypeScript?

Ресурсы