Разбираем, почему TypeScript оставляет undefined после .filter(Boolean), и учимся использовать type predicate для правильных типов. Читайте, чтобы писать безопасный код.
Вчера при настройке CORS я получил от ИИ такой код:
const allowedOrigins = [
process.env.FRONTEND_URL || "http://localhost:3000",
process.env.ADMIN_URL || "http://localhost:3001",
].filter(Boolean);Я задумался: зачем тут .filter(Boolean), если fallback уже гарантируют строки? Навёл на переменную — тип string[]. Всё логично. Но потом я убрал fallback:
const allowedOrigins = [
process.env.FRONTEND_URL,
process.env.ADMIN_URL,
].filter(Boolean);Тип изменился на (string | undefined)[]. Я был в шоке. Как так? Ведь filter(Boolean) на runtime удаляет все falsy-значения, включая undefined. Почему TypeScript думает иначе?
Boolean как функция-колбэк удаляет из массива любые falsy-значения: false, null, undefined, 0, "", NaN. Например:
["https://app.com", "", undefined].filter(Boolean)
// Result: ["https://app.com"]На runtime всё работает идеально. Ни одного undefined не выживет. Так почему же TypeScript с этим не согласен?
TypeScript — это транспилятор. Он не запускает .filter(Boolean), а только анализирует типы. Когда он видит:
array.filter(Boolean)Он знает, что колбэк возвращает boolean. Но он не понимает, что это означает для типов выживших элементов. Он не может вывести: «если Boolean(x) истинно, то x — строка». Поэтому undefined остаётся в типе, хотя в рантайме его никогда не будет. Это разрыв: поведение корректно, но типы врут.
TypeScript позволяет закрыть этот разрыв с помощью type predicate — явного указания компилятору, что гарантирует функция фильтрации:
const allowedOrigins = [
process.env.FRONTEND_URL,
process.env.ADMIN_URL,
].filter((origin): origin is string => Boolean(origin));
// Тип: string[] ✅Часть origin is string — это предикат. Это обещание компилятору: «если функция вернула true, значение точно строка». TypeScript доверяет этому и сужает тип.
Если такой паттерн встречается часто, вынесите его в хелпер:
function isDefined<T>(value: T | undefined | null): value is T {
return value != null;
}Тогда:
const allowedOrigins = [
process.env.FRONTEND_URL,
process.env.ADMIN_URL,
].filter(isDefined);
// Тип: string[] ✅Переиспользуемо, самодокументируемо и красиво. Лично я предпочитаю такой вариант.
Вернёмся к исходному коду с ||:
const allowedOrigins = [
process.env.FRONTEND_URL || "http://localhost:3000",
process.env.ADMIN_URL || "http://localhost:3001",
].filter(Boolean);process.env.X || "fallback" всегда вычисляется в string. Fallback-строка покрывает случай undefined, поэтому TypeScript уже знает, что каждый элемент — строка, ещё до фильтрации. .filter(Boolean) здесь просто защитный манёвр — полезен, если кто-то позже добавит запись без fallback, но для корректности типов не нужен.
(string | undefined)[]. Используйте, когда тип результата не важен.string[]. Для однократного использования.string[]. Для переиспользования в кодовой базе.string. Когда нужен гарантированный запасной вариант..filter(Boolean) — это рантайм-штука, которую TypeScript воспринимает как чёрный ящик. Если вам нужно, чтобы типы отражали реальное содержимое массива, используйте type predicate. Небольшое изменение — честные типы.
Спасибо за чтение! Теперь идите и примените isDefined в своём проекте.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →