RxJS 过滤器函数不会缩小类型,除非直接给它一个类型保护,它是唯一的参数

RxJS filter function not narrowing type unless directly given a typeguard its the only parameter

我一直在开发一个 auth 服务,它使用 rxjs 行为主题来存储最后检索到的 auth 对象,并在它已过期(或根本没有被提取)时触发重新提取。

我的问题是关于 TypeScript 类型检查器的。我已经编写了断言的类型保护 isNotUndefined - 好吧,正是你所期望的。

export function isNotUndefined<T>(input: T | undefined): input is T {
  return input !== undefined;
}

我已经不得不编写上面的类型保护而不是能够依赖 auth !== undefined。我现在无法理解为什么在下面代码的 authGetter$ 中的管道中,管道中值的类型在第一个过滤器后没有减少到 Auth .相反,类型仍然是 Auth | undefined,它需要第二个过滤器和类型保护来将类型缩小到 Auth.

总而言之,为什么我需要第二个过滤器来将类型缩小到 Auth?此外,因为我是在没有人审查的情况下自己编写代码,所以我非常感谢任何人指出 'code smells' 他们认可(并建议如何做)。

export default class AuthService {
  private static lastAuth$ = new BehaviorSubject<Auth | undefined>(undefined);

  private static authGetter$ = AuthService.lastAuth$.pipe(
    filter(auth => {
      if (isNotUndefined(auth) && auth.expiry > new Date()) {
        return true ; // identical resulting type with "return isNotUndefined(auth);"
      } else {
        // retry if auth doesn't exist or is expired
        AuthService.authorise().then(newAuth =>
          AuthService.lastAuth$.next(newAuth)
        );
        return false;
      }
    }),
    tap(v => {}), // typechecker says "(parameter) v: Auth | undefined"
    filter(isNotUndefined),
    tap(v => {}) // typechecker says "(parameter) v: Auth"
  );

  static getAuth$(): Observable<Auth> {
    return this.authGetter$.pipe(first());
  }

  private static async authorise(): Promise<Auth> {
    // auth code goes here (irrelevant for this question)...
    // typecast dummy return to make typechecker happy
    return Promise.resolve(<Auth>{});
  }
}

我附上了我的代码的照片,它以漂亮的语法突出显示,方便您查看:)

User-defined type guard functions 至少目前是严格由用户定义的。它们不是由编译器自动推断的。如果你想要一个 boolean-returning 函数作为一个带有类型谓词 return 类型的类型保护,你需要这样显式地注释它:

const doesNotPropagate = <T>(x: T | undefined) => isNotUndefined(x);
// const doesNotPropagate: <T>(x: T | undefined) => boolean

函数 doesNotPropagate() 在运行时的行为与 isNotUndefined() 相同,但编译器不再将其视为类型保护,因此如果将其用作过滤器,则不会消除undefined 在编译器中。

GitHub 中有多个关于此的问题;当前未解决的问题跟踪 propagating/flowing 类型保护签名是 microsoft/TypeScript#16069 (or possibly microsoft/TypeScript#10734)。不过这里好像没什么动静,所以暂时我们只需要按原样处理语言即可。


这里有一个玩具示例,可以用来探索处理这个问题的不同可能方式,因为您问题中的示例代码不构成 minimal reproducible example 适合放入独立 IDE 。您应该能够使这些适应您自己的代码。

假设我们有一个类型为 Observable<string | undefined> 的值 o。然后这有效:

const a = o.pipe(filter(isNotUndefined)); // Observable<string>

但这不是因为上面列出的原因...类型保护签名不会传播:

const b = o.pipe(filter(x => isNotUndefined(x))); // Observable<string | undefined>

如果我们像这样手动注释箭头函数,我们可以重新获得类型保护签名和行为:

const c = o.pipe(filter((x): x is string => isNotUndefined(x))); // Observable<string>;

如果需要,您可以从这里执行额外的过滤逻辑:

const d = o.pipe(filter((x): x is string => isNotUndefined(x) && x.length > 3)); 
// Observable<string>;

此处过滤器检查字符串是否已定义并且其长度是否大于 3。

请注意,从技术上讲,这不是一个行为良好的用户定义类型保护,因为它们倾向于将 false 结果视为输入范围缩小到 exclude 守护类型:

function badGuard(x: string | undefined): x is string {
  return x !== undefined && x.length > 3;
}
const x = Math.random() < 0.5 ? "a" : undefined;
if (!badGuard(x)) {
  x; // narrowed to undefined, but could well be string here, oops
}

这里如果badGuard(x)returnstrue,就知道x就是string。但是如果 badGuard(x) returns false, 你 不知道 知道 xundefined... 但是这就是编译器的想法。

确实在你的代码中你并没有真正处理过滤器 returns false 的情况(我猜后续的管道参数只是不触发?),所以你真的不必太担心这个。尽管如此,最好将代码重构为一个正确的类型保护,然后是执行额外逻辑的非类型保护过滤器:

const e = o.pipe(filter(isNotUndefined), filter(x => x.length > 3)); // Observable<string>;

这在运行时应该得到相同的结果,但这里第一个过滤器正确地从 Observable<string | undefined> 缩小到 Observable<string>,第二个过滤器保持 Observable<string>(并且 x 在回调中是一个 string) 并执行过滤长度的额外逻辑。

这还有一个额外的好处,即不需要类型注释,因为您不会试图在任何地方传播类型保护签名。所以这可能是我推荐的方法。


好的,希望对您有所帮助;祝你好运!

Stackblitz link to code