TypeScript отфильтровывает нули из массива

TypeScript, режим --strictNullChecks.

Предположим, у меня есть массив обнуляемых строк (string | null)[]. Что может быть способом с одним выражением для удаления всех нулей таким образом, чтобы у результата был тип string []?

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = ???;

Array.filter не работает здесь:

// Type '(string | null)[]' is not assignable to type 'string[]'
array.filter(x => x != null);

Могут работать массивы, но они не поддерживаются TypeScript.

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

24 ответа

Решение

Вы можете использовать функцию предиката типа в .filter чтобы избежать отказа от строгой проверки типов:

function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
    return value !== null && value !== undefined;
}

const array: (string | null)[] = ['foo', 'bar', null, 'zoo', null];
const filteredArray: string[] = array.filter(notEmpty);

В качестве альтернативы вы можете использовать array.reduce<string[]>(...),

Еще одна надежная мера, о которой часто забывают flatMap который может справиться filter а также map за один раз (это также не требует преобразования в string[]):

// (string | null)[]
const arr = ["a", null, "b", "c"];
// string[]
const stringsOnly = arr.flatMap(f => typeof f === "string" ? [f] : []);

Подобно ответу @bijou-trouvaille, вам просто нужно объявить <arg> is <Type> в качестве вывода функции фильтра:

array.filter((x): x is MyType => x !== null);

Только что понял, что вы можете сделать это:

      const nonNull = array.filter((e): e is Exclude<typeof e, null> => e !== null)

Чтобы вы:

  1. получить однострочный, без дополнительных функций
  2. не обязательно знать тип элементов массива, так что копировать можно везде!

Один лайнер:

      const filteredArray: string[] = array.filter((s): s is string => Boolean(s));

Игровая площадка TypeScript

Уловка состоит в том, чтобы передать предикат типа ( :s is string синтаксис).

Этот ответ показывает, что Array.filter требует от пользователей предоставления предиката типа.

Вы можете бросить свой filter результат в тип, который вы хотите:

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray = array.filter(x => x != null) as string[];

Это работает для более общего случая использования, который вы упомянули, например:

const array2: (string | number)[] = ["str1", 1, "str2", 2];
const onlyStrings = array2.filter(x => typeof x === "string") as string[];
const onlyNumbers = array2.filter(x => typeof x === "number") as number[];

( код на детской площадке)

Вот решение, которое я считаю даже немного более кратким, чем принятый ответ от @ bijou-Trouvaille

      const notEmpty = <T>(value: T): value is NonNullable<typeof value> => !!value 
      const array: (string | null | undefined)[] = ['foo', 'bar', null, 'zoo', undefined];

const filteredArray: string[] = array.filter(notEmpty);
console.log(filteredArray)
[LOG]: ["foo", "bar", "zoo"]

Вы можете использоватьisконструкция, позволяющая сузить тип:

Чтобы всем не приходилось писать вспомогательные функции одного и того же типа снова и снова, я объединил функции, называемые isPresent, isDefined а также isFilledво вспомогательную библиотеку: https://www.npmjs.com/package/ts-is-present

Определения типов в настоящее время:

export declare function isPresent<T>(t: T | undefined | null): t is T;
export declare function isDefined<T>(t: T | undefined): t is T;
export declare function isFilled<T>(t: T | null): t is T;

Вы можете использовать это так:

import { isDefined } from 'ts-is-present';

type TestData = {
  data: string;
};

const results: Array<TestData | undefined> = [
  { data: 'hello' },
  undefined,
  { data: 'world' }
];

const definedResults: Array<TestData> = results.filter(isDefined);

console.log(definedResults);

Когда Typescript объединит эту функциональность в, я удалю пакет. Но пока наслаждайтесь.

Если вы уже используете Lodash, вы можете использовать. Или, если вы предпочитаете Рамду, добавка Рамда также имеет compact функция.

У обоих есть типы, поэтому ваш tsc будет счастлив и в результате получит правильные типы.

Из файла Lodash d.ts:

      /**
 * Creates an array with all falsey values removed. The values false, null, 0, "", undefined, and NaN are
 * falsey.
 *
 * @param array The array to compact.
 * @return Returns the new array of filtered values.
 */
compact<T>(array: List<T | null | undefined | false | "" | 0> | null | undefined): T[];

просто используйте

      array.filter(Boolean);

Это будет работать для всех истинных ценностей.

Это, к сожалению, не обеспечивает вывод типа, нашел это решение здесь

      
type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T; //from lodash 

function truthy<T>(value: T): value is Truthy<T> {
    return Boolean(value);  //  or !!value
}

const arr =["hello","felow","developer","",null,undefined];

const truthyArr = arr.filter(truthy);

// the type of truthyArr will be string[]

Я считаю, что у вас все это хорошо, за исключением того, что проверка типов просто делает отфильтрованный тип не отличным от возвращаемого типа.

const array: (string | null)[] = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array.filter(f => f !== undefined && f !== null) as any;
console.log(filterdArray);

Если вы можете принять накладные расходы другого.map()элегантное решение — использовать ненулевой оператор утверждения .

      const array = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array.filter(s => s != null).map(s => s!);

Если вы хотите сохранить undefines, вы можете использоватьtypeofпо переменной и типу утилитыExcludeчтобы удалить нули из типа.

      const array = ["foo", "bar", null, "zoo", null];
const filterdArray: string[] = array
  .filter(s => s !== null)
  .map(s => s as Exclude<typeof s, null>);

Думаю, это будет простой подход с более чистым кодом.

const array: (string | null)[] = ['foo', 'bar', null, 'zoo', null];
const filteredArray: string[] = array.filter(a => !!a);

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

Некоторые ответы предполагают reduceвот как:

      const languages = ["fr", "en", undefined, null, "", "de"]

// the one I prefer:
languages.reduce<string[]>((previous, current) => current ? [...previous, current] : previous, [])

// or
languages.reduce((previous, current) => current ? [...previous, current] : previous, Array<string>())

// or
const reducer = (previous: string[], current: string | undefined | null) => current ? [...previous, current] : previous
languages.reduce(reducer, [])

Результат: ["fr", "en", "de"]

ТС Детская площадка здесь .

      const filterdArray = array.filter(f => !!f) as string[];

Самый короткий путь:

      const validData = array.filter(Boolean)

Я много раз возвращался к этому вопросу, надеясь, что какая-то новая функция Typescript или набор текста могут это исправить.

Вот простой трюк, который мне очень нравится при комбинировании карты с последующим фильтром.

      const animals = ['cat', 'dog', 'mouse', 'sheep'];

const notDogAnimals = animals.map(a => 
{
   if (a == 'dog')
   {
      return null!;   // just skip dog
   }
   else {
      return { animal: a };
   }
}).filter(a => a);

Вы увидите, что я возвращаюсь null!который на самом деле становится типом never- это означает, что окончательный тип не имеет значения null.

Это небольшая вариация исходного вопроса, но я довольно часто попадаю в этот сценарий, и это помогает избежать другого вызова метода. Надеюсь, когда-нибудь Typescript придумает лучший способ.

В TypeScript есть несколько утилит для определения типа массива и исключения из него значений:

      const arrayWithNulls = ["foo", "bar", null, "zoo", null]

type ArrayWithoutNulls = NonNullable<typeof arrayWithNulls[number]>[]

const arrayWithoutNulls = arrayWithNulls.filter(x => x != null) as ArrayWithoutNulls

Дольше, но безопаснее, чем просто заливка вручную as string[] в вашем новом массиве.

Шаг за шагом:

  1. Получим типы из исходного массива:
      typeof arrayWithNulls[number] // => string | null
  1. Исключить null значения:
      NonNullable<typeof arrayWithNulls[number]> // => string
  1. Сделайте это массивом:
      NonNullable<typeof arrayWithNulls[number]>[] // => string[]

Ссылки:

  • NonNullable (официальный документ)
  • typeof array[number] (сообщение в блоге, я ничего не нашел об этом в официальном документе)

Если вы проверяете null с другими условиями, используя просто фильтр, это можно использовать, надеюсь, это поможет тем, кто ищет решения для object array

array.filter(x => x != null);
array.filter(x => (x != null) && (x.name == 'Tom'));

Объединив один из моих любимых ответов выше, с некоторыми общими приемами и расширением интерфейса Array, я смог сделать глобальное определение, которое после добавления в ваш модуль позволяет «сплющить» любой массив, удалив все нулевые значения, заменив(any|undefined|null)[]сany[].

Вот так:mixedArray.squish()хорошо для цепочки и карты.

Просто добавьте этот код где-нибудь в свой модуль (не стесняйтесь пропустить материал eslint, но мой набор беспокоил меня в нескольких вещах здесь):

      /* eslint-disable no-unused-vars */
/* eslint-disable no-extend-native */
declare global {
  interface Array<T> {
    squish<NonNull, Nullable extends (NonNull | undefined | null)>(): NonNull[];
  }
}

if (!Array.prototype.squish) {
  Array.prototype.squish = function squish<NonNull, T extends(NonNull|undefined|null)>
  (this: T[]): NonNull[] {
    return this.flatMap((e) => (e ? [e] : [])) as NonNull[]
  }
}

Или вы можете попробовать пакет: @p4ck93/ts-is

https://www.npmjs.com/package/@p4ck493/ts-is

В примере используется метод CDN, но пакет также поддерживает машинописный текст.

Вы также можете использовать двойное отрицание (!!), чтобы отфильтровать все ложные значения:

  • значение NULL
  • неопределенный
  • Пустая строка "" ('')
  • Число 0
  • Число NaN например,
          array?.filter(item => !!item).map((item: any)
      const arr1 = [.....]
const arr2 = [.....]
const arr3 = [.....]

const newArr = [
   ...(arr1 || []),
   ...(arr2 || []),
   ...(arr3 || []),
]

Другие вопросы по тегам