无法分配给 NonNullable<T>,即使我检查过该值已定义

Can't assign to NonNullable<T> even though I checked that value is defined

为了达到 TypeScript 的最佳性能,我目前正在进入该语言的一些小众领域,但有些事情我还没有明白。

在严格空检查和严格模式以及启用了这些东西的情况下,我不明白为什么这段代码会给我一个错误:

function filterNullable<T>(arr: T[]): NonNullable<T>[] {
    const ret: NonNullable<T>[] = [];
    arr.forEach(el => {
        if (el !== null && el !== undefined) {
            ret.push(el); // TS2345: Argument of type 'T' is not assignable to parameter of type 'NonNullable '.
        }
    })
    return ret;
}

显然,即使 T 是类似于 string | number | undefined 的类型,当我试图将过滤后的值推入数组时,很明显它不能undefined。 TypeScript 是否不够聪明,无法跟随我来到这里(我不这么认为,因为类似的东西工作得很好),我是否遗漏了一个关键细节,或者 NonNullable 类型是否没有人们预期的那么强大?

(我知道有更简洁的方法来做我在这里做的事情,但是我需要具有类似逻辑的代码来做更复杂的事情,所以 Array.prototype.filter 风格的方法不是相当适用)

--------编辑: 然而,我现在有点明白问题所在了,而不是在 push 方法中使用类型断言,我宁愿将整个事情重写为

之类的东西
function filterNullable2<T>(arr: Array<T | undefined | null>): T[] {
    const ret: T[] = [];
    arr.forEach(el => {
        if (el !== null && el !== undefined) {
            ret.push(el);
        }
    })
    return ret;
}

这似乎做得更好。但是还需要多写。

我真正想做的是找到一种更好的方法来为 NGRX 编写实用程序,因为这真的很烦人(谢天谢地,没有第三种无效类型)

import {MemoizedSelector, Store} from "@ngrx/store";
import {filterNullable} from "../shared/operators";
import {Observable} from "rxjs";

export function selectDefined<T, S> (store: Store<T>, selector: MemoizedSelector<T, NonNullable<S>>): Observable<S>;
export function selectDefined<T, S> (store: Store<T>, selector: MemoizedSelector<T, NonNullable<S> | null>): Observable<S>;
export function selectDefined<T, S> (store: Store<T>, selector: MemoizedSelector<T, NonNullable<S> | undefined>): Observable<S>;
export function selectDefined<T, S> (store: Store<T>, selector: MemoizedSelector<T, NonNullable<S> | undefined | null>): Observable<S> {
  return store.select(selector).pipe(filterNullable());
}

我希望找到一种方法来写这个

export function selectDefined<T, S> (store: Store<T>, selector: MemoizedSelector<T, S>): Observable<NonNullable<S>> {
  return store.select(selector).pipe(filterNullable());
// TS2322: Type 'Observable<S>' is not assignable to type 'Observable<NonNullable<S>>'.   Type 'S' is not assignable to type 'NonNullable<S>'.
}

不幸的是,这不起作用

nullundefined 检查变量 el 后仍然是 T 类型,因为您仍然可以分配 nullundefined 到 if 语句中的变量。

要解决这个问题,只需告诉编译器你正在推送一个 NonNullable<T>:

ret.push(el as NonNullable<T>)

为了让您的代码更简洁一点:

function filterNullable<T>(arr: T[]): NonNullable<T>[] {
    return arr.filter((s) : s is NonNullable<T> => !!s)
}

这本质上是 TypeScript 的设计限制。里面执行一个generic function, it is difficult for the compiler to reason about types that depend on unspecified type parameters like T. See microsoft/TypeScript#48048求权威解答


对于像string | undefined这样的特定类型的值,编译器可以使用control flow analyasis将值的类型过滤为stringundefined。实际上,如果我们将通用 T 替换为特定的 string | undefined:

,您的代码就可以正常工作
function filterNullable(arr: (string | undefined)[]): NonNullable<string | undefined>[] {
  const ret: NonNullable<string | undefined>[] = [];
  arr.forEach(el => {
    if (el !== null && el !== undefined) {
      ret.push(el); // okay
    }
  })
  return ret;
}

不幸的是,对于像 T 这样的通用类型的值,没有要应用的 specific 过滤。如果编译器知道 Tconstrained to a union 类型,那么空检查可能会将 elT 缩小到某些 特定的 约束的子类型,像这样:

function filterNullable<T extends string | undefined>(arr: T[]): NonNullable<T>[] {
  const ret: NonNullable<T>[] = [];
  arr.forEach(el => {
    if (el !== null && el !== undefined) {
      el.toUpperCase() // this works
      ret.push(el); // this still doesn't
    }
  })
  return ret;
}

看看自 T extends string | undefined 以来,编译器是如何意识到 el 必须在 null 检查后可分配给 string 的。太好了,在 TypeScript 4.3 中添加了对它的支持 contextual narrowing for generics。但这对你来说还不够好,因为你需要 el 不是缩小到特定类型 string 而是缩小到通用类型 NonNullable<T> (这肯定是 [=14= 的某个子类型) ] 但可能会更窄,例如,如果 T"foo" | "bar" | undefined).


我们被困在这里了。编译器根本不合成类型 NonNullable<T> 来响应空检查。 The NonNullable<T> utility type is implemented as a conditional type,作为控制流分析的结果,编译器未合成条件类型。

曾在 microsoft/TypeScript#22348 which would have enabled such narrowings. But it was never merged:

有一个拉取请求

This has inspired many a conversation over the years, however as-is we know we have no shot of merging this. Conditional types have too many drawbacks for it to be OK to introduce them in control flow (like being very hard to relate correctly).

所以目前还不可能。


当然有变通办法,其中最简单的就是使用 type assertion。我不会在这里讨论其他可能性,因为它基本上超出了问题的范围。

Playground link to code