TypeScript,——strictNullChecks模式。

假设我有一个可空字符串数组(string | null)[]。什么是单表达式的方式,以这样的方式删除所有的空值,结果有类型字符串[]?

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

数组中。过滤器在这里不起作用:

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

数组推导式可以工作,但TypeScript不支持。

实际上,这个问题可以推广为通过从联合中删除具有特定类型的项来过滤任何联合类型的数组的问题。但是让我们关注带有null和可能未定义的联合,因为这些是最常见的用例。


当前回答

一个衬套:

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

打印稿操场

诀窍是传递一个类型谓词(:s是字符串语法)。

这个答案显示了数组。过滤器要求用户提供类型谓词。

其他回答

结合上面我最喜欢的答案之一,与一些通用技巧和对Array接口的扩展,我能够做出一个全局定义,添加到您的模块后,允许任何数组被“压扁”,删除所有空值替换(任何|undefined|null)[]与任何[]。

mixedarray . squash()适合链接和映射。

只需在模块的某个地方添加这段代码(可以随意省略eslint的东西,但我的set在这里的一些事情让我感到困扰):

/* 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[]
  }
}

你可以在.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实际上并不能保证它在编译时正确地识别值是null还是undefined。例如,尝试将其return语句缩短为return value !== null;,您将不会看到编译器错误,即使函数将在undefined上错误地返回true。

缓解这种情况的一种方法是首先使用控制流块约束类型,然后使用一个虚拟变量让编译器进行检查。在下面的例子中,编译器能够推断出value参数在赋值时不能为null或未定义。但是,如果您从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;
}

提醒一句:在某些情况下,这种方法仍然会失败。一定要注意与逆变相关的问题。

为了避免每个人都不得不一遍又一遍地编写相同类型的保护helper函数,我将称为isPresent, isDefined和isfill的函数绑定到一个helper库: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捆绑这个功能时,我将删除这个包。但是,现在,享受吧。

TypeScript有一些实用工具来推断数组的类型并排除null值:

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

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

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

比在新数组上手动转换为string[]更长,但更安全。

循序渐进:

从原始数组中获取类型:

typeof arrayWithNulls[number] // => string | null

排除空值:

NonNullable<typeof arrayWithNulls[number]> // => string

让它成为一个数组:

NonNullable<typeof arrayWithNulls[number]>[] // => string[]

链接:

NonNullable(官方文档) typeof array[number](博客文章,我在官方文档中找不到任何关于它的东西)

你可以将你的筛选结果转换为你想要的类型:

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[];

(操场上的代码)