Настоящий JavaScript
前往频道在 Telegram
Тот самый канал по JavaScript. Личный блог автора - @just_genych По вопросам рекламы или разработки: @g_abashkin
显示更多6 168
订阅者
-724 小时
-337 天
-5730 天
数据加载中...
相似频道
标签云
进出提及
---
---
---
---
---
---
吸引订阅者
七月 '26
七月 '26
+3
在0个频道中
六月 '26
+110
在0个频道中
Get PRO
五月 '26
+384
在0个频道中
Get PRO
四月 '26
+83
在0个频道中
Get PRO
三月 '26
+71
在0个频道中
Get PRO
二月 '26
+23
在0个频道中
Get PRO
一月 '26
+18
在0个频道中
Get PRO
十二月 '25
+13
在0个频道中
Get PRO
十一月 '25
+24
在0个频道中
Get PRO
十月 '25
+22
在0个频道中
Get PRO
九月 '25
+21
在0个频道中
Get PRO
八月 '25
+20
在0个频道中
Get PRO
七月 '25
+38
在0个频道中
Get PRO
六月 '25
+52
在0个频道中
Get PRO
五月 '25
+121
在0个频道中
Get PRO
四月 '25
+88
在0个频道中
Get PRO
三月 '25
+363
在20个频道中
Get PRO
二月 '25
+30
在0个频道中
Get PRO
一月 '25
+38
在0个频道中
Get PRO
十二月 '24
+41
在0个频道中
Get PRO
十一月 '24
+64
在2个频道中
Get PRO
十月 '24
+70
在1个频道中
Get PRO
九月 '24
+84
在2个频道中
Get PRO
八月 '24
+70
在0个频道中
Get PRO
七月 '24
+115
在0个频道中
Get PRO
六月 '24
+89
在4个频道中
Get PRO
五月 '24
+101
在0个频道中
Get PRO
四月 '24
+121
在0个频道中
Get PRO
三月 '24
+132
在0个频道中
Get PRO
二月 '24
+101
在0个频道中
Get PRO
一月 '24
+86
在0个频道中
Get PRO
十二月 '23
+90
在0个频道中
Get PRO
十一月 '23
+16
在0个频道中
Get PRO
十月 '23
+15
在0个频道中
Get PRO
九月 '23
+16
在0个频道中
Get PRO
八月 '23
+24
在0个频道中
Get PRO
七月 '23
+35
在0个频道中
Get PRO
六月 '23
+20
在0个频道中
Get PRO
五月 '23
+15
在0个频道中
Get PRO
四月 '23
+264
在0个频道中
Get PRO
三月 '23
+17
在0个频道中
Get PRO
二月 '23
+207
在0个频道中
Get PRO
一月 '23
+40
在0个频道中
Get PRO
十二月 '22
+478
在0个频道中
Get PRO
十一月 '22
+1 627
在0个频道中
Get PRO
十月 '22
+45
在0个频道中
Get PRO
九月 '22
+451
在0个频道中
Get PRO
八月 '22
+459
在0个频道中
Get PRO
七月 '22
+1 991
在0个频道中
Get PRO
六月 '22
+1 567
在0个频道中
Get PRO
五月 '22
+1 769
在0个频道中
Get PRO
四月 '22
+97
在0个频道中
Get PRO
三月 '22
+2 701
在0个频道中
| 日期 | 订阅者增长 | 提及 | 频道 | |
| 09 七月 | 0 | |||
| 08 七月 | 0 | |||
| 07 七月 | 0 | |||
| 06 七月 | +1 | |||
| 05 七月 | 0 | |||
| 04 七月 | 0 | |||
| 03 七月 | 0 | |||
| 02 七月 | +1 | |||
| 01 七月 | +1 |
频道帖子
User-Defined Type Guards vs Branded Types: скрытые costs производительности и баги с узкими типами
В больших codebase на TypeScript type guards и branded types часто создают излишнюю уверенность в типах, скрывая реальные рантайм-проблемы и влияя на производительность. Ошибки возникают при их использовании в production: фронтенд-приложения с API-клиентами, серверная валидация на Node.js, shared libraries — везде, где надо отделить один тип от другого.
Проблема с type guards
TypeScript “верит” твоему предикату, не проверяя его корректность внутри. После прохождения guard он забывает о других типах, что может привести к багам.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(data: string | number | null) {
if (isString(data)) {
return data.toUpperCase(); // data: string, но если guard не учел null, рантайм баг
}
}
Типичная ошибка: guard не обрабатывает null или undefined. В больших унионах это “съедает” целые ветки, и баг уходит в прод.
Branded types: иллюзия безопасности
Типа type Email = string & { __brand: 'email' } — это попытка номинальной типизации. Но это работает только на уровне типов, без runtime-гарантий.
type Email = string & { __brand: 'email' };
function isEmail(v: string): v is Email {
return v.includes('@');
}
// Любая строка с '@' считается Email, но brand отсутствует в runtime
В большом проекте один неверный guard — и цепочка типов ломается, особенно при пересечениях с generics. Компилятор тупит на inference, что замедляет сборку и увеличивает время анализа.
Performance trade-offs
1. Кастомные guards не inlined — лишние вызовы в горячих путях (циклы, часто вызываемые функции) и growth bundle. 2. Branded types в generics приводят к замедлению type inference при вложенности. 3. Для реальной безопасности всё равно нужны runtime-проверки, которые дублируют логику и увеличивают latency.
Совет: используй простые встроенные проверки (typeof, instanceof) для простых типов. Branded types оборачивай в конструкторы с runtime-валидацией:
function createEmail(v: string): Email {
if (!v.includes('@')) throw new Error();
return v as Email;
}
И обязательно покрывай guards unit-тестами на граничные значения: null, undefined, пустые строки — это 30% багов с типами.
Вывод: Type guards и branded types — инженерные компромиссы, где безопасность типов в compile-time стоит runtime-производительности и скрытых багов, если не добавлять параллельные runtime-проверки.| 2 | WeakMap и WeakSet для безопасного кэширования в production: неочевидные утечки через closured references и производительность при частой сборке мусора
Если вы храните кэш в Map внутри замыкания, то даже после того как объект-ключ становится недоступен, ссылка остается — объект не собирается GC. В больших SPA, API-клиентах или SSR-рендерах это приводит к постепенному росту памяти без видимой причины.
Почему WeakMap решает проблему утечек
Замена Map на WeakMap автоматически удаляет запись при исчезновении последней внешней ссылки на ключ. Это критично для кэширования результатов при обработке временных объектов, например, в middleware или event handlers.
function createWeakCache() {
const cache = new WeakMap();
return {
get: (obj) => cache.get(obj),
set: (obj, value) => cache.set(obj, value)
};
}
Подводный камень: GC при высокой частоте ключей
WeakMap/WeakSet очищаются только во время сборки мусора. Если вы создаете короткоживущие объекты (меньше 100 мс жизни) и сразу добавляете их в WeakMap, то GC будет запускаться чаще — до десятков раз в секунду. Это проседает CPU в Node.js API или FPS в браузере.
* Плохой паттерн — каждый микротик создавать объекты и помещать их в WeakSet.
* Пример: seen.add(obj) в цикле генерации 1000 объектов за 1 мс — GC будет чистить постоянно.
Практический совет и проверка
1. Не используйте WeakMap/WeakSet для high-frequency cache (миллионы ключей за секунду). Лучше обычный Map с ручной очисткой или LRU-стратегией.
2. Проверяйте профиль GC: Chrome DevTools > Performance > Memory. Если видите частые малые пики GC при короткоживущих объектах — пересмотрите архитектуру.
3. Для долгоживущих объектов (DOM-ноды, классы с временем жизни > 1 секунды) WeakMap безопасен и выгоден.
Производительность
Операции в WeakMap в 2-4 раза медленнее Map (микросекунды). Но ключевая стоимость — GC: чем больше ключей без внешних ссылок, тем чаще GC проверяет их. Это может добавить десятки миллисекунд на сборку при нескольких тысячах ключей.
Вывод:
WeakMap и WeakSet — мощный инструмент против утечек через замыкания, но при high-frequency кэшировании короткоживущих объектов вы рискуете получить нагрузку на GC, а не спасение памяти. | 154 |
| 3 | Сидеть и работать в корпорации страшно, жизнь-то мимо проходит. Уходить строить бизнес страшно, а вдруг прогорит. Один из вариантов - разрабатывать свой пет-проект по вечерам. Многие успешные компании, например, Twitter, создавались именно так. Это не значит, что ваш проект обязательно заработает миллиарды, но заработать больше, чем в найме, и получить ценный опыт вполне реально.
Перед началом разработки появляется множество вопросов, например:
– Как выбрать идею для пет-проекта?
– Что нужно знать про маркетинг?
– Как запуститься и довести до первых продаж не имея бюджета на рекламу?
В телеграм-канале Твой пет проект, Михаил Табунов делится своим опытом с разработчиками и менеджерами.
Он рассказывает, где искать идею для нового проекта, что нужно знать о маркетинге, как запустить стартап и привлечь первых 10 клиентов, а также о многих других важных вещах.
Подписывайтесь на Твой пет проект получайте пользу от практиков рынка! | 187 |
| 4 | AbortSignal.timeout и AbortController.any: гонки, утечки и производительность при композиции асинхронных операций
Эти API кажутся удобными для таймаутов и отмены, но в production под нагрузкой они порождают неочевидные баги: двойные вызовы обработчиков, утечки памяти и потерю контроля над ресурсами. Особенно это заметно в Node.js сервисах с высокой конкуренцией запросов или в SPA с частыми опросами.
Классический таймаут: скрытая утечка
async function fetchWithTimeout(url, timeoutMs) {
const signal = AbortSignal.timeout(timeoutMs);
return fetch(url, { signal });
}
Запрос завершился за 50 мс, а таймаут на 5000 мс ещё активен. В Node 18+ сигнал очищается автоматически, но в браузерах, особенно Safari, слушатель abort может висеть до срабатывания. При 1000 таких вызовов — привет, рост памяти и недетерминированные гонки при отписке.
AbortController.any: двойной abort
const combined = AbortController.any([
controller.signal,
AbortSignal.timeout(5000)
]);
Первый сигнал сработал, создался composite signal. Если второй догоняет до фактической отписки — по спецификации флаг aborted должен быть один, но на практике реализации в разных средах (Edge, Safari) кидают событие abort дважды. Обработчик отрабатывает два раза: гонка в бизнес-логике и лишний вызов колбэка.
Production-практика: контроль через ручной таймер
Для горячих путей (частые запросы, опросы графиков) лучше завести один AbortController и таймер через setTimeout с явным reject:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await fetch(url, { signal: controller.signal });
return result;
} finally {
clearTimeout(timer);
}
Так вы сами управляете жизненным циклом таймера и избегаете лишних слушателей. Минус — больше кода, плюс — предсказуемое поведение без гонок.
Вывод: Иногда старый setTimeout с ручной очисткой оказывается надёжнее встроенного AbortSignal.timeout, особенно под нагрузкой с частыми вызовами и конкурентными таймаутами. | 210 |
| 5 | Анатомия парного доклада: как мы собирали «PROvoke» на Analyst Days. Часть 2 — выступление и фидбек
Привет, Хабр! В первой части я рассказывал, как мы с Татьяной Маркиной готовили доклад «Провоок» на Analyst Days — про споры, студию в Краснодаре, логистический ад и идею с кнутом и пряником. А сегодня — второй части: что произошло на сцене, какие роли мы играли, почему корсет так и не вышел в свет, и как один разговор в кулуарах перевернул моё понимание публичных выступлений.
Читать далее | 236 |
| 6 | Intl.ListFormat и Intl.DateTimeFormat: как локализация ломает SSR и гидратацию
Локализация через Intl API обычно кажется безобидной, пока не сталкиваешься с расхождениями между сервером и клиентом. В production на SSR React, Next.js или Remix race condition по локали между серверным рендерингом и гидратацией — частая причина трудновоспроизводимых багов, особенно в приложениях с мультиязычностью и SEO.
Race condition по локали
Сервер получает локаль из хедера Accept-Language, а клиент — из navigator.language. Если на сервере сформировали Intl.ListFormat('de').format(['a', 'b', 'c']) (немецкий "a, b und c"), а гидратация пошла с английским "a, b, and c", React выдаёт warning о несовпадении. Фиксируй локаль глобально до гидратации, например через Intl.DateTimeFormat.supportedLocalesOf и ручной выбор.
Локаль по умолчанию — ловушка
new Intl.DateTimeFormat() в Node.js без аргументов использует en-US. На клиенте с русской локалью браузер выдаёт DD.MM.YYYY. Разница в форматировании — гарантированный hydration mismatch. Всегда передавай локаль явно: new Intl.DateTimeFormat('ru-RU'). Иначе гидратация поломается будет warning.
Типичная ошибка: мокинг только одного API
В тестах часто мокают только DateTimeFormat, забывая ListFormat. Это приводит к тому, что список "1, 2, 3" сервер превратит в "1, 2 und 3", а клиент — в "1, 2, and 3". Мокай оба API сразу через jest.mock('intl', () => ({ ... })) или вообще выноси локализацию в отдельный слой, который заменяется целиком.
Практический совет: кешируй инстансы
Каждый new Intl.DateTimeFormat('ru') создаёт объект с внутренним состоянием. На SSR с сотнями запросов GC не успевает, появляются лаги. Используй кеш: const cache = new Map(); cache.get(locale) || cache.set(locale, new Intl.DateTimeFormat(locale)).get(locale). Это снижает нагрузку на память и ускоряет рендеринг.
Предупреждение: порядок слов ломает гидратацию
Intl.ListFormat('ja').format(['x', 'y', 'z']) даёт японский формат с иероглифом-разделителем. Сравнивать строки на сервере и клиенте без учёта локали — сразу mismatch. Проверяй, что локаль одинакова, иначе hydration failed.
Вывод: Явная локализация, кеширование инстансов и одновременный мокинг Intl.ListFormat и Intl.DateTimeFormat — ключевые практики для надёжной локализации в SSR и тестах. | 229 |
| 7 | Temporal API: неочевидные edge cases с часовыми поясами, границами дней и производительность в production
Temporal — долгожданная замена Date и moment.js, но она не делает магию, а лишь делает её предсказуемой. В production три сценария могут сломать логику: DST-разрывы, ложные сравнения и скрытые аллокации.
1. Граница дня — это не 24 часа: DST создаёт несуществующие моменты
Перевод часов в часовом поясе с летним временем — классическая ловушка. Попытка создать ZonedDateTime с несуществующим временем, например 02:30 в America/New_York при переходе на DST:
const zdt = Temporal.ZonedDateTime.from({
year: 2024, month: 3, day: 10,
timeZone: 'America/New_York',
hour: 2, minute: 30
}); // RangeError: момент не существует
Решение — опция { disambiguation: 'earlier' } или 'later'. Но 'later' даст 03:30 вместо ожидаемых 02:30. Логика с startOfDay() обязана это учитывать. Иначе баг воспроизводится раз в полгода.
2. Сравнение ZonedDateTime: equals() игнорирует часовой пояс
equals() сравнивает календарные поля, а не абсолютное время:
const a = Temporal.ZonedDateTime.from('2024-01-01[UTC]');
const b = Temporal.ZonedDateTime.from('2024-01-01[Asia/Tokyo]');
a.equals(b); // true — часы и даты совпадают, но момент разный
a.epochMilliseconds === b.epochMilliseconds; // false — физически разные моменты
Это ошибка: в [UTC] и [Asia/Tokyo] один и тот же календарный день — это разные моменты, отстоящие на +9 часов. Для точного сравнения всегда приводите к Instant или используйте .since(). Иначе половина пользователей получит неверные расписания.
3. Производительность: Temporal не бесплатен — профилируйте
Каждый ZonedDateTime.from() с нестандартным часовым поясом парсит базу IANA TZif — это поиск по файлу. .until() / .since() не ленивы: сразу вычисляют разницу по всем полям. А toPlainDate() не кешируется — каждый вызов аллоцирует новый объект.
На 100k+ событиях в секунду с разными таймзонами замена Date на Temporal дала рост CPU на 40% на Node.js 20. Для редких вычислений Temporal отличен, но для hot path используйте Intl.DateTimeFormat + Date.getTime().
Вывод: Temporal решает 90% старых проблем, но DST-разрывы, сравнение по календарю и неучтённые аллокации — ваши новые враги; профилируйте и тестируйте на границах дней с реальными данными. | 221 |
| 8 | Как я хакнул рынок труда: пишем свой ИИ-комбайн для автооткликов на HH.ru
Всем привет! Если вы хоть раз искали работу в IT за последний год, то знаете, что рынок беспощаден к новичкам. Нужно откликнуться на сотни вакансий, а в итоге получаешь отказы от роботов. Чтобы пробиться через фильтры HR, нужно под каждую вакансию писать уникальное сопроводительное письмо.
В этой статье я сделаю полный разбор того, как я написал собственного автономного ИИ-агента, который ищет вакансии, фильтрует мусор с помощью локальной нейросети, пишет персонализированные сопроводительные письма и отчитывается мне в Telegram, пока я спокойно занимаюсь своими делами. Я хотел, чтобы скрипт был бесплатным, автономным и не требовал танцев с бубном вокруг платных API.
Читать далее на Habr | 282 |
| 9 | Branded Types в TypeScript: как заставить типы работать в рантайме
Типы в TypeScript исчезают при компиляции, но в production мы сталкиваемся с данными из внешнего мира, где структурная типизация не способна защитить от путаницы между двумя сущностями одного базового типа — например, Email и UserId, объявленными как строки. Branded Types с Zod-схемами позволяют сохранить семантику в compile-time и runtime-валидацию для production-кода.
Проблема структурной типизации
Структурная типизация TypeScript не отличает Email от UserId, если оба — строки. Это приводит к ошибкам, когда в функцию отправки письма передаётся идентификатор пользователя. Branded Types добавляют невидимую метку на уровне типов, но не обеспечивают проверку в рантайме. Zod даёт реальную валидацию при работе с API, формами или env.
Решение с Zod и branded types
Используйте Zod для парсинга данных, извлекая типы через z.infer и добавляя brand-метку. Пример:
import { z } from 'zod';
const EmailSchema = z.string().email().brand('Email');
type Email = z.infer<typeof EmailSchema>;
function sendEmail(to: Email) { /* */ }
const email = EmailSchema.parse('user@example.com');
sendEmail(email); // compile-time + runtime pass
Типичная ошибка — попытка передать сырую строку напрямую в sendEmail. Branded type отсечёт это на этапе компиляции, а схема Zod перехватит некорректные данные из API ещё до логики.
Где применять и trade-offs
Подходит для моделей UserId, OrderId, SKU, чтобы избежать путаницы в API-клиентах, SDK или легаси-миграциях. Но:
- Не защищает от злонамеренной подделки — это не криптография, а дисциплина типов.
- Легко переборщить: для простых DTO достаточно обычных структурных типов.
- Без командных договорённостей бренды становятся кашей — кто-то использует Zod, кто-то самописные guards. Практический совет: ограничьте бренды только критичными сущностями на границе модулей.
Вывод: Branded Types с Zod решают проблему runtime-гарантий для доменных моделей за счёт дисциплины типов и zero-cost метки, но требуют строгих конвенций в команде. | 255 |
| 10 | Изящный баг в Edge Runtime: prototype pollution через structuredClone и Symbol-свойства
Вы доверяете structuredClone защиту от прототип-поллюшена? Зря. В Edge Runtime старых версий (V8) баг позволяет Symbol-ключам с именем __proto__ при клонировании записаться как строковые ключи, обходя стандартные проверки. Это особенно опасно в API-серверах на Node.js и edge-функциях, где входящие данные не проходят валидацию.
Как это работает
Обычная защита от prototype pollution строится на проверке ключей через Object.keys() или for...in. Но эти методы игнорируют Symbol-ключи. Злоумышленник вставляет символ с именем __proto__:
const payload = {
[Symbol('__proto__')]: { polluted: true }
};
const clean = structuredClone(payload);
В уязвимых версиях Edge Runtime этот символ неправильно обрабатывается как строковой ключ, и объект clean получает прототип-загрязнение.
Почему это изящно
- Object.keys() и for...in не видят Symbol-ключи — стандартная проверка их пропускает.
- JSON.parse(JSON.stringify()) теряет Symbol — многие разработчики перешли на structuredClone, считая его безопаснее.
- Но structuredClone обрабатывает Symbol-ключи (кроме well-known). Механизм безопасного клонирования становится вектором атаки через незаметные Symbol.
Типичная ошибка
Думать, что structuredClone изолирует от prototype pollution, если вы проверяете только строковые ключи. В code review я встречал код, где после глубокого клонирования входных данных объект проверялся Object.keys() — Symbol остались незамеченными.
Production-совет
Для временных данных из ненадёжных источников используйте Object.create(null) — он блокирует прототипную цепочку. Дополнительно, проверяйте Symbol-ключи вручную: Object.getOwnPropertySymbols(obj). Если вы используете edge runtime — обновляйте его до актуальной версии, где этот баг исправлен.
Вывод: structuredClone не является панацеей от prototype pollution — Symbol-ключи с именами __proto__ могут обходить защиту, если вы не обрабатываете их явно. | 246 |
| 11 | Lazy evaluation с Proxy и мемоизацией: скрытые costs при дебаге, сериализации и неожиданные mutation-баги в production
Ленивое вычисление через Proxy обещает изящное отложенное исполнение, но в production эта абстракция превращается в источник трудноуловимых багов. Часто разработчики не учитывают побочные эффекты для дебага, сериализации и мутаций, что приводит к критическим инцидентам в API-клиентах, SSR или shared libraries.
1. Дебаг превращается в квест
Когда Proxy вычисляет значение лениво, в stack trace видно только финальный результат. Chrome DevTools сам вызывает геттер при инспекции, что может спровоцировать нежелательные вычисления второй раз. Пример:
const proxy = new Proxy({}, {
get(target, key) {
if (!(key in target)) { target[key] = compute(key); }
return target[key];
}
});
// console.log в getter может вызвать повторный compute
Совет: используй дебаг через отладчик с явными брейкпоинтами, а не через console.log, который может перезапустить мемоизацию.
2. Сериализация умирает молча
JSON.stringify(proxy) вернет {}, если не перехватывать ownKeys и getOwnPropertyDescriptor. Пропусти это — и данные исчезнут при передаче в REST или localStorage. Типичная ошибка: думать, что Proxy автоматически виден для сериализации. Практический совет: явно реализуй обработчики:
const proxy = new Proxy(target, {
get(target, key) { /*... лениво вычисляем */ },
ownKeys() { return [...Object.keys(target), ...dynamicKeys]; },
getOwnPropertyDescriptor(_, key) { return { configurable: true, enumerable: true }; }
});
Иначе — потеря данных без ошибки.
3. Неожиданные mutation-баги с reference-запахом
Когда ты кэшируешь вычисленный объект, а затем кто-то мутирует его, следующий доступ вернет измененное значение. Пример: const data = proxy.items; data.push('new'); — следующий вызов proxy.items вернет ту же ссылку, что ломает идемпотентность. Это особенно опасно в state-менеджменте или при параллельных запросах. Предупреждение: никогда не возвращай мутабельные ссылки из Proxy без глубокого копирования, иначе получишь гонки данных, которые сложно воспроизвести локально.
Вывод: Proxy с мемоизацией — мощный, но хрупкий паттерн; всегда тестируй сериализацию и мутации, а в критических путях лучше используй явные геттеры. | 217 |
| 12 | WeakRef и FinalizationRegistry: слабые ссылки как production-ready инструмент управления памятью
В асинхронных пайплайнах — от Node.js-сервисов до SSR и heavy client-side кэширования — каждый временный объект (кэши, буферы, промисы, слушатели) держится сильной ссылкой, блокируя сборщик мусора. Типичная ошибка: держать в Map результаты дорогих API-вызовов, не понимая, что Map хранит сильные ссылки, и память растёт бесконтрольно.
WeakRef: слабая ссылка для кэширования без утечек
WeakRef позволяет GC очищать объект, если на него нет сильных ссылок. Идеально для кэша, где данные можно пересоздать — экономит память под нагрузкой:
const cache = new Map<string, WeakRef<any>>();
const registry = new FinalizationRegistry((key: string) => {
cache.delete(key);
});
async function fetchData(key: string): Promise<any> {
let ref = cache.get(key);
let cached = ref?.deref();
if (cached) return cached;
const data = await expensiveAPICall(key);
const weakRef = new WeakRef(data);
cache.set(key, weakRef);
registry.register(data, key);
return data;
}
GC решает, когда удалить объект. Если памяти хватает — данные живут. Если нет — кэш "сдувается" без ручной очистки и утечек.
FinalizationRegistry: детекция утечек и чистка ресурсов
Колбэк при удалении объекта — можно отслеживать, что объект не был освобождён вовремя:
class LeakDetector {
private registry: FinalizationRegistry<string>;
private active = new Map<string, number>();
constructor(onLeak: (name: string) => void) {
this.registry = new FinalizationRegistry((name: string) => {
const count = (this.active.get(name) ?? 0) - 1;
if (count <= 0) this.active.delete(name);
else this.active.set(name, count);
onLeak(name);
});
}
track(obj: object, name: string) {
this.active.set(name, (this.active.get(name) ?? 0) + 1);
this.registry.register(obj, name);
}
}
Практический совет: используй в отладке для обнаружения утечек долгоживущих объектов — подписок, web-сокетов, буферов.
Типобезопасная обёртка с generic
TypeScript дружит с WeakRef через generic, что даёт надёжные типы:
class WeakCache<T extends object> {
private refs = new Map<string, WeakRef<T>>();
private registry = new FinalizationRegistry<string>((key) => {
this.refs.delete(key);
});
set(key: string, value: T): void {
const ref = new WeakRef(value);
this.refs.set(key, ref);
this.registry.register(value, key);
}
get(key: string): T | undefined {
return this.refs.get(key)?.deref();
}
}
Предупреждение: WeakRef не работают с примитивами (string, number) — только объекты. Для примитивов используй WeakMap или обёртки. Также не используй WeakRef для критичных по времени кэшей — GC непредсказуем.
Вывод: Слабые ссылки — не экзотика, а инженерный инструмент для надёжного управления памятью, где главное правило — "используй, если данные можно пересоздать, и никогда не полагайся на детерминизм GC". | 207 |
| 13 | Buffer в браузере: портирование Node.js-кода на Web Streams API и неочевидные грабли с TextEncoder/TextDecoder в production
Перетащить обработку бинарных данных из Node.js в браузер — берешь Buffer, оборачиваешь в Uint8Array и production падает. Проблема в том, что Buffer — глобал из Node.js, его нет в браузере, а полифилы тащат лишние килобайты. Современный путь — Web Streams API и TextEncoder/TextDecoder, но есть грабли, которые ломают production.
Грабли 1. slice не уважает UTF-8
В Node.js Buffer.from('Привет!').slice(0, 5) дает 5 байт и строку 'Прив'. В браузере new TextEncoder().encode('Привет!').slice(0, 5) режет посреди многобайтового символа — получаешь битый байт. В production это вылезает при обрезании логов с кириллицей или эмодзи. Решение: не используй slice на Uint8Array вслепую, применяй subarray и TextDecoder с stream: true для восстановления границ.
Грабли 2. Потоковое декодирование без stream: true
Web Streams отдают данные чанками. Если декодировать каждый чанк как отдельную строку, последний может быть урезан — многобайтовый символ разобьется на два чанка. Типичная ошибка:
// Ломается на границе чанков
for await (const chunk of reader) {
const str = new TextDecoder().decode(chunk);
}
Нужно так:
const decoder = new TextDecoder('utf-8', { stream: true });
for await (const chunk of reader) {
const str = decoder.decode(chunk, { stream: true });
}
const final = decoder.decode(); // завершающий вызов
Пропустишь stream: true — получишь битые данные на границах чанков, например, при парсинге CSV с кириллицей в production.
Грабли 3. Утечка памяти из-за создания инстансов
Создание new TextEncoder() внутри цикла или хендлера запросов вызывает GC-шторм. Вынеси в константы модуля один раз. Тоже самое с TextDecoder — кэшируй для всех чанков потока.
Практические советы при портировании
- Buffer.concat заменяй на ручное объединение через new Uint8Array(totalLength) и set.
- fs.createReadStream заменяй на fetch + response.body (ReadableStream).
- Для энкодинга/декодинга используй только TextEncoder/TextDecoder с stream: true на чанках.
- Никогда не доверяй прямому slice по Uint8Array, если внутри UTF-8 — это ломает строки с акцентами, кириллицей и эмодзи в production.
Вывод: Грабли не в том, что Buffer нет в браузере, а в том, что TextEncoder.encode ведет себя иначе — учитывай многобайтовые символы, кэшируй инстансы и всегда декодируй с stream: true. | 222 |
| 14 | Symbol.toStringTag — тот самый символ, который ломает всё подряд
Казалось бы, просто добавил [Symbol.toStringTag] в класс, получил [object MyCoolClass] — и радуешься. Но production показывает, что это "удобство" превращает дебаг в квест: особенно когда фреймворк или библиотека ожидают строгий "Object", "Array" или "Promise", а получают что-то своё.
Сериализация и JSON
Прямого влияния на JSON.stringify() нет. Но если пишешь свой toJSON(), где проверяешь this[Symbol.toStringTag] — вот тут баг. Класс-наследник с другим тегом превращается в родительский тип. Я такое ловил, когда сериализовал DTO: потомок считался предком, и бэкенд падал с валидацией.
* Типичная ошибка: проверять тип через тег, а не через this.constructor.name.
* Практический совет: в кастомной сериализации используй отдельное поле _typeTag.
Прототипы под прицелом
Многие фреймворки определяют тип через Object.prototype.toString.call(obj). Зачем? Потому что typeof не отличает Array от обычного объекта. А если ты переопределил тег в прототипе (например, сделал Array.prototype[Symbol.toStringTag] = "MyArray") — библиотека, ожидающая "Array", просто сломает иммутабельность или реактивность. Видел такое в старых полифиллах.
* Предупреждение: не трогай Symbol.toStringTag в прототипах Array, Promise, Map — движок использует их для внутренних проверок.
* Trade-off: изменяя тег на прототипе встроенных типов, ты создаёшь скрытые зависимости от строковых сравнений в чужих модулях.
shouldOverrideVisibility и production-фреймворки
В недрах React Native или Vue 2 composition API есть функция, которая проверяет видимость компонента через Symbol.toStringTag. Кейс: обернул компонент в HOC, HOC перетирает тег. shouldOverrideVisibility сравнивает тег с ожидаемым — и игнорирует компонент как "чужой". Элементы рендерятся, но никогда не становятся видимыми. Это не баг документации — это примитивная проверка на уровне типа.
Пример бага:
class Base {
get [Symbol.toStringTag]() { return "Base" }
}
class Child extends Base {}
// Child будет сериализоваться как Base
* Чтобы защититься в HOC: прокидывай тег через Object.defineProperty с оригинальным значением.
Вывод: Маленький символ Symbol.toStringTag — это скрытая точка отказа при сериализации, защите прототипов и в production-фреймворках, поэтому относись к нему как к мине — лучше не трогать, если не контролируешь все уровни стека. | 252 |
| 15 | Feature flags на TypeScript: типобезопасная композиция с branded types и runtime validation для production-конфигураций
Feature flags в production — удобно, но конфиги часто превращаются в мешанину строк. Простое { isNewFeature: true } не защищает от опечаток вроде isNewFeautre, и линтер молчит до прогона тестов или деплоя. Используем branded types с Zod для строгой проверки на этапе компиляции и выполнения.
Branded types для compile-time безопасности
Тип Brand<T, B> добавляет уникальную метку. Передать простой boolean вместо NewFeature не выйдет — TS потребует явного приведения. Это исключает путаницу между флагами:
type Brand<T, B> = T & { __brand: B };
type NewFeature = Brand<boolean, 'NewFeature'>;
type LegacyFallback = Brand<boolean, 'LegacyFallback'>;
function useFeature(newFeature: NewFeature, legacyFallback: LegacyFallback): boolean {
return newFeature || legacyFallback;
}
Runtime validation с Zod
Даже с branded types в JSON может прилететь невалидное значение. Добавляем схему с z.boolean().brand() — тогда при парсинге ошибка будет явной, а не тихим undefined:
const ConfigSchema = z.object({
newFeature: z.boolean().brand<'NewFeature'>(),
legacyFallback: z.boolean().brand<'LegacyFallback'>(),
});
type Config = z.infer<typeof ConfigSchema>;
function loadConfig(raw: unknown): Config {
return ConfigSchema.parse(raw);
}
Дискриминируемые union для состояний флагов
Когда два флага конфликтуют (например, newFeature и legacyFallback), обычные типы не мешают включить оба одновременно. Discriminated union сужает область до корректных комбинаций — это предотвращает два источника истины:
type FlagState =
| { newFeature: true; legacyFallback: false }
| { newFeature: false; legacyFallback: boolean };
function processState(state: FlagState) {
if (state.newFeature) {
return state.legacyFallback; // TS сужает до false
}
return state.legacyFallback;
}
* Предупреждение: branded types добавляют шум в код и усложняют вложенные конфиги без линтера. Для плоских A/B тестов и канареечных релизов это окупается — ошибка ловится на этапе компиляции, а не в production.
* Совет: комбинируй discriminated union с zod-схемой на весь конфиг — тогда даже опечатка в JSON упадёт с понятным сообщением, а не молча сломает логику.
Вывод: Типобезопасная композиция флагов через branded types и runtime-валидация даёт двухуровневую защиту — compile-time строгость и runtime-проверку входных данных, что критично для надёжности A/B экспериментов и канареечных деплоев. | 257 |
| 16 | Скрытые баги в AbortSignal.timeout() и типобезопасная композиция AbortController в асинхронных цепочках
При работе с асинхронными операциями в Node.js или браузере часто используют AbortSignal.timeout() для ограничения времени выполнения. Это удобно, но таит подводные камни, особенно в цепочках promise или при композиции нескольких timeout-сигналов. Разработчики нередко забывают про race condition между завершением запроса и отменой, а также про невозможность отличить таймаут от ручного прерывания без явной настройки причины.
Проблема: гонка состояний и необработанные ошибки
Когда AbortSignal.timeout(5000) передаётся в fetch, а запрос завершается раньше таймаута, сигнал просто игнорируется — это нормально. Но если запрос падает с ошибкой до срабатывания таймаута, сам таймаут всё равно вызывает abort. Возникает ситуация: ошибка уже обработана в catch, а AbortError от таймаута остаётся необработанным. В консоли появляется unhandled rejection.
Пример: если fetch упадёт с 500-м ответом на 3-й секунде, а таймаут срабатывает на 5-й, то второй reject — от таймаута — не будет пойман. Это классический race condition, который рвет цепочку error handling.
Отсутствие различимости таймаутов
AbortSignal.timeout() бросает DOMException с именем AbortError. Свой AbortController при вызове .abort() тоже создаёт такой же exception. В итоге в catch нельзя отличить, что пошло не так: таймаут или ручная отмена. Единственный способ — задать signal.reason при abort, но по умолчанию он undefined. Это делает диагностику сложной, особенно в production.
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') {
// Что это? Таймаут? Ручной abort? Непонятно.
}
}
Типобезопасная композиция: решение для цепочек
Для асинхронных операций, где нужно несколько таймаутов (например, сначала fetch, потом обработка данных), AbortSignal.any() — не выход: он не сохраняет причину и не убирает race condition. Вместо этого используйте композицию контроллеров с явным контролем таймаутов.
Пример простой утилиты для оборачивания promise с таймаутом:
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(new Error('Timeout')), ms);
return Promise.race([
promise,
new Promise<never>((_, reject) => {
controller.signal.addEventListener('abort', () => reject(controller.signal.reason));
})
]).finally(() => clearTimeout(id));
}
Здесь таймаут генерирует кастомную ошибку с понятным сообщением, которую можно отловить. Для цепочек — складывайте контроллеры в массив и управляйте ими вручную:
const acc = new AbortController();
const fetchPromise = fetch(url, { signal: acc.signal });
const timeoutId = setTimeout(() => acc.abort(new Error('Fetch timeout')), 5000);
fetchPromise.finally(() => clearTimeout(timeoutId));
Типичная ошибка при композиции
Попытка объединить AbortSignal.timeout() с собственным контроллером через AbortSignal.any() приводит к потере контроля над причиной и времени отмены. Если таймаут от AbortSignal.timeout() сработает первым, ваш контроллер никогда не будет вызван, что приводит к утечке ресурсов (например, незавершённый HTTP-запрос).
Вывод: Не используйте AbortSignal.timeout() в production без явной обработки причины и контроля race condition — для асинхронных цепочек надёжнее собрать композицию вручную с кастомными ошибками и явным управлением таймаутами. | 274 |
| 17 | Получи грант до 3,48 млн на обучение дизайну
Поступай на дизайн в Центральный университет с грантом.
Для учеников 10–11-х классов и СПО. Освой графический, UI/UX и продуктовый дизайн. Создавай визуальные концепты будущего.
На программе студенты получают фундаментальную базу, развивают прикладные навыки, приобретают опыт работы над реальными проектами, собирают портфолио и строят связи внутри дизайн-сообщества
Подать заявку
#реклама 16+
cu.ru
О рекламодателе | 201 |
| 18 | Conditional types в рантайме: как runtime type guards оживляют discriminated unions
Discriminated unions удобны в TypeScript, но в production их часто приходится проверять вручную через цепочки if и switch. Это ломает типы и множит ошибки на границе с внешними данными — API-ответами, JSON, AST-нодами. Решение: combine mapped types с runtime для автоматической генерации type guards.
Mapped type как контракт для runtime
Вместо того чтобы писать отдельный guard для каждого варианта union, определяем объект-гард через mapped type:
type ShapeGuards = {
[K in Shape['kind']]: (obj: unknown) => obj is Extract<Shape, { kind: K }>
};
Теперь для каждой ветки (circle, rect, group) нужно только реализовать валидацию полей. Тип обяжет вернуть правильный предикат.
Единая точка входа и generic фабрика
Добавляем общую функцию:
function isShape<T extends Shape['kind']>(kind: T, obj: unknown): obj is Extract<Shape, { kind: T }> {
return shapeGuards[kind](obj);
}
Типизация работает полностью: после isShape('circle', data) TypeScript знает, что data — это { kind: 'circle'; radius: number }. Для рекурсивных веток вроде group с вложенными Shape[] это тоже безопасно.
Можно уйти дальше — сделать generic-фабрику, которая возвращает строго типизированный guard для конкретного kind без кастингов внутри:
type GuardOfKind<T> = T extends Shape['kind']
? (obj: unknown) => obj is Extract<Shape, { kind: T }>
: never;
function createKindGuard<T extends Shape['kind']>(kind: T): GuardOfKind<T> {
return shapeGuards[kind] as any;
}
Типичная ошибка и trade-off
Ошибка: проверять только kind, а не поля варианта. TypeScript поведет, но в runtime придет некорректный объект — и сломается что-то ниже. Всегда проверяй специфичные поля: radius, shapes и т.д.
Минус подхода: для глубоко вложенных дифференцированных union придется писать много ручной валидации. Лечится либо кодогенерацией, либо внешними библиотеками (zod, io-ts). Но для легаси с 10+ ветками такой подход выигрывает у хаоса из switch и if-else.
Вывод: Соединение conditional типов с runtime guards превращает абстрактную типизацию в production-ready механизм проверки на границе доверия — особенно в API-клиентах, SDK и парсерах AST. | 250 |
| 19 | Типизация FSM без библиотек: почему discriminated unions подводят в production и как это чинить
Discriminated unions в TypeScript отлично работают для статичных конечных автоматов, но в реальном production — с динамической сменой стейтов, асинхронными переходами и конфигами из плагинов — быстро проявляют лимиты. Частая ошибка: думать, что union решает все проблемы FSM, а потом ловить race conditions и невалидные переходы.
Закрытый union и динамические стейты
Union дискриминирован только на этапе компиляции. Если стейты подгружаются в рантайме — например, из конфига или через плагинную архитектуру — тип не расширить. Решение: карта стейтов через mapped types.
type StateMap = {
idle: Record<string, never>
loading: { progress: number }
error: { message: string }
}
type State = {
[K in keyof StateMap]: { status: K } & StateMap[K]
}[keyof StateMap]
Новый стейт в StateMap — и тип обновляется автоматически. Но типовая безопасность переходов остаётся хрупкой.
Нетипизированные переходы и гонки
DU типизирует только состояние, а не граф переходов. После loading можно прыгнуть в error, хотя бизнес-логика требует success. В production это даёт гонки при параллельных запросах. Попытка типизировать через guard-функции:
function transition<S extends State, T extends State>(
prev: S, next: T, guard: (prev: S) => boolean
): T {
if (!guard(prev)) throw new Error('Invalid transition')
return next
}
Но это не спасает от асинхронных race conditions — требуется флаг блокировки. Типы не гарантируют порядок, только композицию.
Контекст и дублирование полей
При 10+ стейтах общие поля (например, id заказа) дублируются в каждом типе. Вынос в дженерик — простое, но эффективное решение:
type BaseState<T extends string, Ctx> = { status: T } & Ctx
type FSM =
| BaseState<'idle', {}>
| BaseState<'loading', { progress: number }>
| BaseState<'error', { message: string }>
Так контекст не размазывается, а union остаётся читаемым.
Вывод: Discriminated unions — стартовая точка, но для production FSM без xstate обязательно нужны карта стейтов, типизированные переходы с guard'ами и вынесенный контекст, иначе на втором километре кода типы начнут врать при динамической смене состояний. | 277 |
| 20 | Как я собираю мини‑аналитику по рынку профессий
Давно работая с HR‑аналитикой, стало интересно не просто смотреть на рынок, но и самому выделить что‑то основное, что можно собрать и представить: зарплатная аналитика, аналитика подбора персонала и тому подобное. Частные случаи отсутствия роста оплаты труда могут восприниматься людьми так, будто такое везде, но это может быть ошибкой. Год назад была достаточно сильная гонка зарплат, которая сейчас привела к акценту на производительности труда в стране. Многие ее не заметили.
Безработица низкая, что означает дефицит кадров. Но сейчас не дефицит кадров вообще, а дефицит квалифицированных кадров и дефицит рабочих. Без данных такие фразы превращаются в ощущения, а ощущения — плохая основа для выводов. Поэтому начат сбор небольшого аналитического проекта по рынку профессий. Идея простая: брать открытые данные, приводить их в порядок и собирать короткие профили по отдельным профессиям.
Читать далее | 315 |
