Здравствуйте, меня зовут Дмитрий Карловский и (сколько себя помню) я борюсь со своим окружением. Ведь оно такое костное, дубовое, и никогда не понимает, что я от него хочу. Но в какой-то момент я понял, что хватит это терпеть и надо что-то менять. Поэтому теперь не окружение диктует мне, что я могу и не могу делать, а я диктую окружению каким ему быть. Как вы уже поняли, далее речь пойдёт про инверсию контроля через "контекст окружения". Многим этот подход уже знаком по "переменным окружения" - они задаются при запуске программы и обычно наследуются для всех программ, которые та запускает. Мы же применим эту концепцию для организации нашего кода на TypeScript. Итак, что мы хотим получить: Функции при вызове наследуют контекст у вызвавшей их функции Объекты наследуют контекст у их объекта-владельца В системе может существовать одновременно множество вариантов контекста Изменения в производных контекстах не влияют на исходный Изменения в исходном контексте отражаются на производных Тесты могут запускаться в изолированном и не изолированном контексте Минимум бойлерплейта Максимум перфоманса Тайпчек всего этого Давайте, объявим какую-нибудь глобальную константу в глобальном контексте окружения: Теперь добавим в глобальный контекст какую-нибудь функцию. Например, функцию записи в лог: Обратите внимание на типизированный this. Он гарантирует, что данную функцию нельзя будет вызвать напрямую так: Вызвать её можно исключительно из какого-либо контекста окружения. Например, из глобального контекста: Однако, пока что $ у нас - это неймспейс, а не тип. Давайте для простоты создадим и одноимённый тип: А раз мы получаем контекст окружения в this, то можем его использовать и для вызова других функций. Например, напишем функцию, которая приветствует пользователя по имени: Таким образом мы естественным образом можем передавать контекст окружения по стеку вызовов функций на любую глубину. Но в этом мало смысла, пока контекст всего один. Поэтому добавим фабрику контекстов, которая берёт текущий контекст, наследует от него производный, патчит его переданными ей переопределениями и возвращает: Object.create мы используем, чтобы создание производного контекста было быстрым, даже если он разрастётся. А вот Object.assign не используется, чтобы в переопределениях можно было задавать не только значения, но и геттеры, и сеттеры. Эта фабрика нам ещё пригодится, а пока давайте напишем наш первый тест: Тест принимает на вход контекст кружения, и первым делом он настраивает контекст под себя - патчит функцию $log, чтобы та сохраняла все логи в локальную переменную. Потом мы запускаем тестируемую функцию в нашем контексте, и наконец, проверяем, что в логи вывелось именно то, что мы ожидаем. Напишем простейшую функцию для асертов: Обратите внимание, что мы поместили тест в отдельный неймспейс $.$test. Это нужно для того, чтобы взять и запустить все тесты скопом: Каждый тест запускается не в оригинальном контексте, а в изолированном. Это такой производный контекст, где замоканы все сущности, что общаются со внешним миром (сетевые запросы, время, консоль, файлы, рандом и т.д.). Исходно, она просто создаёт новый производный контекст: Но наша функция $log пишет в реальную консоль, что не очень-то похоже на изоляцию. Поэтому, рядом с ней мы положим переопределение $isolated, которое переопределяет в контексте $log на реализацию без сайд эффектов: Теперь мы уверены, что любые тесты по умолчанию не будут ничего писать в реальную консоль даже если мы не переопределим в них функцию $log. Давайте так же напишем и тест, что наши переопределения контекстов работают исправно: Теперь перейдём к объектам. Для простоты работы с контекстами введём простой базовый класс для всех наших классов: Тут мы инъектируем контекст окружения через конструктор. И добавляем геттер, позволяющий получать зависимости через контекст минимальным объёмом кода. Геттер нам нужен для того, чтобы можно было переопределять контекст в потомках не потеряв переопределения предков. Для примера, создадим карточку, которая приветствует пользователя, добавляя к имени восклицательный знак: Напишем тест, чтобы удостовериться, что это действительно работает: Супер, теперь посмотрим, как выстраивать дерево объектов. Тут основная идея в том, что у каждого объекта есть владелец, который контролирует его время жизни и контекст окружения. Давайте создадим страничку, которая владеет нашей карточкой: Выносим создание владеимого объекта в отдельное свойство. Инъектим в него текущий контекст. И мемоизируем результат с помощью $mem. Возьмём самую простую его реализацию без реактивности: WeakMap нужен чтобы такое свойство можно было безопасно переопределять в подклассах, не ломая мемоизацию. Что ж, проверим, что имя пользователя действительно поменялось, а восклицательный знак не потерялся: Отлично, работает. Теперь усложняем задачу - переопределяем класс для поддерева объектов. Создадим новый класс для карточки, который позволяет переопределять имя пользователя, сохраняя его в локальное хранилище. Само локальное хранилище - это просто алиас для нативного объекта: А раз оно у нас персистится, то нужно нужно рядом положить и мок, который сохраняет данные не в нативное хранилище, а во временный объект: Теперь мы, наконец, можем реализовать наше приложение, которое подменяет в контексте исходный класс $hello_card на свой $app_card, и всё поддерево объектов будет инстанцировать именно его. Напишем пользовательский сценарий, где мы сначала сохраняем состояние локального хранилища и стираем его, потом проверяем, что имя соответствует дефолтному поведению, при переименовывании оно меняется и сохраняется даже при перезапуске приложения, а в конце подчищаем за собой, убеждаемся, что очистка сработала, и восстанавливаем состояние локального хранилища: Тесты неспроста написаны в таком стиле, чтобы их можно было запускать не только на чистом состоянии, но и на грязном. Это позволит нам гонять одни и те же тесты с разными уровнями изоляции. Запустим тесты в полностью изолированном контексте, чтобы проверить, что реализовали всю нашу логику правильно: А теперь запустим их же, но без изоляции, чтобы проверить, что наша логика корректно работает со внешними системами. Для этого просто создадим контекст, где $isolated возвращает производный контекст, но без каких-либо переопределений: Этот второй вариант, если запустить в Сафари в порно режиме, выдаст исключение, так как в нём нельзя обращаться к localStorage, а этот кейс в нашей наивной реализации $storage_local не предусмотрен. Аналогично, запуская тесты в разных контекстах, можно проверять работу вашего кода с публичным сервером, с тестовым сервером, и вообще без сервера. Подробнее об этом подходе к тестированию можно ознакомиться в моём выступлении на TechLeadConf: Фрактальное Тестирование. Разобранный же тут подход к инверсии контроля активно применяется во фреймворке $mol, что даёт ему потрясающую гибкость и простоту кода. Но это уже совсем другая история… Если вас смущает общий неймспейс и отcутствие import/export, то можете ознакомиться с этим анализом: Fully Qualified Names vs Imports. А если смущает именование через подчёркивание, то с этим: PascalCase vs camelCase vs kebab case vs snake_case. TypeScript песочница со всем кодом из статьи.  Full Stack Overflow