Avatar ·

Как отфильтровать null и undefined из массива в TypeScript: полное руководство

Всем привет! Я только начал изучать TypeScript и столкнулся с проблемой при работе с массивами. У меня есть массив, который может содержать значения `null` или `undefined`, и мне нужно получить новый массив только с реальными значениями. Вот пример кода: ```typescript const mixedArray: (number | null | undefined)[] = [1, null, 2, undefined, 3, null, 4]; // Я пытаюсь отфильтровать null и undefined const filteredArray = mixedArray.filter(item => item !== null && item !== undefined); // Но TypeScript всё равно считает, что в filteredArray могут быть null/undefined // filteredArray имеет тип (number | null | undefined)[] // Хотя я явно отфильтровал все пустые значения! ``` Я уже пробовал разные варианты: 1. `filter(item => item)` — но это отфильтровывает и `0` и `false`, что мне не нужно 2. `filter(item => item != null)` — тоже не помогает с типами 3. Явное приведение типа `as number[]` — работает, но это костыль Как правильно решить эту проблему? Нужно, чтобы TypeScript понимал, что после фильтрации в массиве гарантированно нет `null` и `undefined`, и тип был `number[]`, а не `(number | null | undefined)[]`. Заранее спасибо за помощь! <<>>

Проблема: TypeScript не выводит правильный тип после filter()

Вы столкнулись с классической проблемой TypeScript: метод filter() не умеет автоматически сужать типы на основе предиката. Даже если вы явно проверяете на null и undefined, TypeScript по умолчанию сохраняет исходный тип массива. Это связано с тем, что сигнатура filter() в стандартной библиотеке не использует пользовательскую защиту типов (type guard).

Решение 1: Type predicate (пользовательская защита типов) — ЛУЧШИЙ ПОДХОД

Создайте функцию-предикат с возвращаемым типом item is T. Это явно сообщает TypeScript, что после фильтрации тип элемента будет T (без null и undefined).

function isNotNullOrUndefined<T>(item: T | null | undefined): item is T {
  return item !== null && item !== undefined;
}

// Использование:
const mixedArray: (number | null | undefined)[] = [1, null, 2, undefined, 3, null, 4];
const filteredArray = mixedArray.filter(isNotNullOrUndefined);
// ✅ filteredArray имеет тип number[]

Как это работает:

  • item is T — это type predicate (предикат типа). Он говорит TypeScript: "Если функция возвращает true, то item гарантированно имеет тип T".
  • Функция универсальна (generic) — работает с любыми типами: string | null, MyInterface | undefined и т.д.
  • TypeScript автоматически выводит тип number[] для filteredArray.

Решение 2: Встроенный type guard через arrow function

Можно использовать стрелочную функцию прямо внутри filter() с явным указанием type predicate:

const filteredArray = mixedArray.filter(
  (item): item is number => item !== null && item !== undefined
);
// ✅ filteredArray: number[]

Важно: Type predicate (item is number) указывается после аргументов, перед телом функции. Без этого TypeScript не сможет сузить тип.

Решение 3: Использование вспомогательных утилит из библиотек

Если вы используете популярные библиотеки утилит:

// С использованием lodash
import { compact } from 'lodash';
const filteredArray = compact(mixedArray); // number[]

// С использованием ramda
import { reject, isNil } from 'ramda';
const filteredArray = reject(isNil, mixedArray); // number[]

Решение 4: Собственная обёртка (если нужно часто)

Создайте кастомный метод на прототипе Array или утилитарную функцию:

// Вариант с глобальным расширением Array
declare global {
  interface Array<T> {
    filterNonNull<U>(this: (U | null | undefined)[]): U[];
  }
}

Array.prototype.filterNonNull = function<T, U>(this: (U | null | undefined)[]): U[] {
  return this.filter((item): item is U => item != null);
};

// Использование:
const filteredArray = mixedArray.filterNonNull(); // number[]

Что выбрать?

  • Для одноразового использования — подойдёт стрелочная функция с type predicate (Решение 2).
  • Для повторяющегося кода — создайте универсальную функцию isNotNullOrUndefined (Решение 1).
  • Если используете библиотеки — проверьте, может быть там уже есть готовое решение (lodash compact).

Важное замечание: Метод filter(Boolean) отфильтрует не только null/undefined, но и 0, "", false — используйте его только если это поведение вам подходит.

<<>>

Альтернативный подход: Использование flatMap() или reduce()

Если вы не хотите использовать type predicate, можно применить flatMap() или reduce() для фильтрации с правильным выводом типов.

Решение через flatMap():

const filteredArray = mixedArray.flatMap(item => 
  item !== null && item !== undefined ? [item] : []
);
// ✅ filteredArray: number[]

flatMap() возвращает массив, и TypeScript корректно выводит тип на основе возвращаемого значения. Если элемент валидный — возвращаем массив с ним, если нет — пустой массив.

Решение через reduce():

const filteredArray = mixedArray.reduce<number[]>((acc, item) => {
  if (item !== null && item !== undefined) {
    acc.push(item);
  }
  return acc;
}, []);
// ✅ filteredArray: number[]

Здесь мы явно указываем тип аккумулятора number[] через generic reduce<number[]>.

Когда это полезно?

  • Если вы не хотите создавать отдельные функции-предикаты.
  • Если вам нужно одновременно фильтровать и трансформировать данные.
  • В проектах, где запрещены пользовательские type guard (редко, но бывает).

Недостатки:

  • Код менее читаемый, чем с type predicate.
  • flatMap() может быть медленнее на больших массивах (создаёт промежуточные массивы).
  • reduce() более многословен.

Рекомендую использовать основной подход с type predicate — он наиболее идиоматичен для TypeScript и легко читается.