TypeScript从数组中过滤掉null值

347

TypeScript, --strictNullChecks 模式。

假设我有一个可为空的字符串数组 (string | null)[]。有什么单表达式的方法可以以这样一种方式删除所有 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 中不被支持。

实际上,这个问题可以概括为从联合类型的数组中过滤掉具有特定类型的条目。但是,让我们专注于包含 null 和 undefined 的联合类型,因为这些是最常见的用例。


遍历数组时,检查索引是否为空,如果不为空,则将它们添加到过滤后的数组中,这有什么问题吗? - Markus G.
3
现在我所做的是迭代+条件判断+插入。我觉得这样太冗长了。 - SergeyS
在 playground 中,使用您发布的 array.filter 的方式非常好。甚至不需要 : string[],只需这样:const filterdArray = array.filter(x => x != null); 编译器会推断出 filterdArray 的类型为 string[]。您使用的 TypeScript 版本是什么? - Nitzan Tomer
6
在游乐场中选择“选项”,并勾选“strictNullChecks”。 - SergeyS
22个回答

388
您可以在 .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[]>(...)

2021更新:更严格的谓词

尽管此解决方案在大多数情况下都有效,但是在谓词中可以获得更严格的类型检查。如所示,函数notEmpty实际上并不保证在编译时正确识别出值是否为nullundefined。例如,请尝试将其返回语句缩短为return value !== null;,您将看不到编译器错误,即使该函数将在undefined上错误地返回true

缓解这种问题的一种方法是首先使用控制流块来约束类型,然后使用虚拟变量让编译器进行检查。在下面的示例中,编译器能够推断出value参数在进行赋值时不能是nullundefined。但是,如果您从if条件中删除|| value === undefined,则会看到编译器错误,提示您上面示例中的错误。

function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
  if (value === null || value === undefined) return false;
  const testDummy: TValue = value;
  return true;
}

警告:存在着仍可能无法解决的情况。请注意与逆变性相关的问题。


1
我认为 value !== null && value !== undefined 不正确,它不会返回一个字符串,而是会返回 true 或 false。此外,传入 null 或 undefined 会给你一个值为 null 或 undefined 的值,因此它并没有真正约束。问题在于这个函数实际上并没有执行过滤,所以它无法真正保证。 - mfollett
notEmpty是否保证将类型从string|null转换为string?其实并不是。使用类型谓词可以保证的是,如果它的类型错误,则无法在过滤器中使用它,这仍然很有用,因为您只需进行几个单元测试就可以轻松填补差距。对定义进行单元测试,使用则由类型系统覆盖。 - Bijou Trouvaille
1
@Bijou 我不明白,但在我的端上它确实将类型限制为 TValue - starikcetin
1
@S.TarıkÇetin 要考虑这样一个事实,如果notEmpty函数的返回值被缩减为 return value !== null;,那么你将不会得到编译器错误提示。 - Bijou Trouvaille
@BijouTrouvaille 我明白了,谢谢你的解释。但我认为那是一个本地化问题,正如你所提到的那样,很容易进行单元测试。 - starikcetin
3
2022年:function notNull<T>(value: T): value is NonNullable<T> { return value != null; } - kien_coi_1997

258

人们经常忘记 flatMap,它可以一次处理 filtermap(而且这也不需要将其转换为 string[]):

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

19
这应该是最佳答案。实际上,我会甚至将其改为 f => !!f ? [f] : [] 以简化代码。 - codeperson
8
值得注意的是,flatMap在ECMA-262(又称为ECMAScript 2021)中定义。对于一些人来说,这可能是一个障碍。 - Alex Klaus
8
可能会影响 tsconfig.json 文件中的 compilerOptions 部分,特别是 "lib" 部分(示例)。 - Alex Klaus
12
为什么Array.filter()不会做这件事?是因为Array.filter()在TypeScript中的支持不够好吗? - WebSpence
9
我相信这段代码也能奏效: arr.flatMap(f => f || []) - Sarsaparilla
显示剩余9条评论

239
与@bijou-trouvaille的答案类似,只需将<arg> is <Type>声明为过滤器函数的输出:
array.filter((x): x is MyType => x !== null);

34
吸引人。但这不是类型安全的。这和使用 "as" 一样糟糕。如果你写了以下代码,TypeScript 不会抱怨:const realArr: number[] = arr.filter((x): x is number => x === undefined);,实际上返回的是一个未定义值数组。 - V Maharajh
2
@VivekMaharajh 这是一个很好的观点,感谢您指出。 - alukach
27
“用户自定义类型保护”在 TypeScript 中并不像您所期望的那样完全“类型安全”:“const isString = (x: number | string): x is string => true;” 这个例子是完全合法的,尽管它会对数字报告 true。如果您的类型保护出了问题,您的类型系统就会有漏洞。这同样适用于这个答案和被接受的答案。您是否有一种“更加类型安全”的方法来实现所需功能? - panepeter
4
我预计许多人阅读此内容时并没有意识到它包含一个未经检查的类型断言。这些人可能会复制/粘贴它,而不是编写更冗长的替代方案,该替代方案不需要任何类型断言。const removeNulls = (arr: (string | null)[]): string[] => { const output: string[] = []; for (const value of arr) { if (value !== null) { output.push(value); } } return output; }; - V Maharajh

40

一句话简述:

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

TypeScript playground

关键在于传递一个类型谓词:s 是字符串的语法)。

这个回答表明 Array.filter 需要用户提供一个类型谓词。


7
可以使用双感叹号!!s代替Boolean(s) - Alex Po
9
@AlexPo,这样就不太清晰了,我建议不要这样做。 - somebody
2
我非常喜欢这个答案,我从未想过在匿名函数中使用类型守卫。它还避免了创建许多数组,就像Mike Sukmanowsky的答案那样。 - Nesk

35

刚刚意识到你可以这样做:

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

这样你就可以:

  1. 得到一个一行的代码,不需要额外的函数
  2. 不必知道数组元素的类型,因此可以在任何地方都复制此代码!

4
这个应该是被接受的想法,因为它使用实用类型来向TypeScript提供类型缩小信息。 - fant0me
同意 - 这是我正在寻找的类型安全解决方案。谢谢! - Rich R

14

你可以将你的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[];

(在 playground 中查看代码)


1
如果有其他选择,最好避免使用 as - Vulwsztyn

12

这里有一个使用NonNullable的解决方案。我觉得它甚至比@bijou-trouvaille所接受的答案更简洁。

function notEmpty<TValue>(value: TValue): value is NonNullable<TValue> {
    return value !== null && value !== undefined;
}
const array: (string | null | undefined)[] = ['foo', 'bar', null, 'zoo', undefined];

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

这不仅会从数组中删除 null 或 undefined。相反,它将删除任何 falsy 值,例如 0、空字符串或布尔值 false。 - Serge Bekenkamp
@SergeBekenkamp 抱歉,我的解释有点不清楚。我只是想指出在这里使用 NonNullable 是有意义的。我相应地修改了我的答案。 - Mathias Dpunkt

11

您可以使用is结构来缩小类型范围:

enter image description here

enter image description here

enter image description here

enter image description here


11
为了避免每个人反复编写相同的类型保护帮助函数,我将名为 isPresentisDefinedisFilled 的函数捆绑到一个帮助程序库中: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将此功能捆绑在一起时,我将删除该包。但现在,请享用。

7
如果您已经使用Lodash,可以使用compact函数。或者,如果您更喜欢Ramda,则ramda-adjunct也具有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[];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接