Функция фильтра RxJS не сужает тип, если только непосредственно не задан типгард, это единственный параметр

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

Мой вопрос касается средства проверки типов TypeScript. Я написал typeguard isNotUndefined, который утверждает, что именно то, что вы ожидаете.

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

Мне уже приходилось писать указанный выше типгард вместо того, чтобы полагаться на auth !== undefined. Теперь я не могу понять, почему в канале в authGetter$ в приведенном ниже коде тип значения в канале не уменьшается до Auth после первого фильтра. Вместо этого тип по-прежнему Auth | undefined, и требуется второй фильтр только с защитой типа, чтобы сузить тип до Auth.

Итак, в общем, зачем мне нужен второй фильтр, чтобы сузить тип до Auth? Кроме того, поскольку я пишу код самостоятельно, и никто не проверяет его, я был бы очень признателен, если бы кто-нибудь указал на «запахи кода», которые они распознают (с предложениями о том, что делать вместо этого).

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>{});
  }
}

Я прилагаю фотографию моего кода с красивой подсветкой синтаксиса для вашего удобства и удовольствия :)

мой код с красивой подсветкой синтаксиса


person Geoff Davids    schedule 23.04.2020    source источник
comment
Ваш lastAuth$ определяется как <Auth | undefined>, поскольку это то, что вы в конечном итоге возвращаете из AuthService.lastAuth$.pipe(filter(...)).   -  person Andrew Nolan    schedule 23.04.2020
comment
Да, но с первым фильтром в канале я блокирую случай ‹undefined›, оставляя только ‹Auth› в качестве единственной другой опции в типе объединения для данных в канале после первого фильтра, нет?   -  person Geoff Davids    schedule 23.04.2020
comment
Не в этом дело. Вы по-прежнему возвращаете тип объединения, определенный в new BehaviorSubject<Auth | undefined>. Ваш тип охраняет меня, нужно поместить куда-нибудь еще, где нужно проверить, есть ли Auth или, возможно, попробовать .map вместо .filter для создания типа Auth   -  person Andrew Nolan    schedule 23.04.2020
comment
Если вы знаете, где разместить защиту типа, чтобы она работала, или если вы знаете, почему она не может вывести подмножество типов на основе анализа потока операторов if, добавьте это в ответ!   -  person Geoff Davids    schedule 23.04.2020
comment
Вы не должны определять свой BehaviorSubject с типом <Auth | undefined>. Это не имеет смысла, потому что вы все равно можете вызывать .next() с null или undefined, даже если он определен только как <Auth>. Это наверняка приведет к проблемам с типом в вашем конвейере, потому что вы не сможете утверждать, что ваше значение не является неопределенным, если вы явно не определите тип с помощью tap((a: Auth) => ..). Но вы действительно не должны делать это в вашем случае.   -  person Reqven    schedule 23.04.2020
comment
@Reqven Я не могу назначить undefined без типа объединения, так как у меня включены строгие нулевые проверки   -  person Geoff Davids    schedule 23.04.2020


Ответы (1)


Определяемые пользователем функции защиты типов являются, по крайней мере в настоящее время, строго определяемыми пользователем. Они не выводятся автоматически компилятором. Если вы хотите, чтобы функция, возвращающая boolean, вела себя как защита типа с возвращаемым типом предиката типа, вам необходимо явно аннотировать ее как таковую:

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

Функция doesNotPropagate() во время выполнения ведет себя так же, как isNotUndefined(), но компилятор больше не видит в ней защиту типа, поэтому, если вы используете ее в качестве фильтра, вы не устраните undefined в компиляторе.

В GitHub есть несколько проблем по этому поводу; в настоящее время открытая проблема с отслеживанием распространения/передачи сигнатур защиты типа: microsoft/TypeScript#16069 ( или, возможно, microsoft/TypeScript#10734). Но не похоже, чтобы здесь было много движений, так что пока нам нужно просто иметь дело с языком таким, какой он есть.


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

Допустим, у нас есть значение o типа Observable<string | undefined>. Тогда это работает:

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 как означающие, что ввод сужается, чтобы исключить защищенный тип:

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) возвращает true, вы знаете, что x равно string. Но если badGuard(x) возвращает false, вы не знаете, что x равно undefined... но так думает компилятор.

Это правда, что в вашем коде вы на самом деле не имеете дело с ситуацией, когда фильтр возвращает false (я полагаю, последующие параметры канала просто не срабатывают?), поэтому вам не нужно слишком беспокоиться об этом. Тем не менее, может быть лучше реорганизовать код в одну правильную защиту типа, за которой следует фильтр без защиты типа, выполняющий дополнительную логику:

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

Это должно привести к тому же результату во время выполнения, но здесь первый фильтр правильно сужает от Observable<string | undefined> до Observable<string>, а второй фильтр сохраняет Observable<string>x в обратном вызове — это string) и выполняет дополнительную логику, которая фильтрует по длине.

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


Хорошо, надеюсь, это поможет; удачи!

Ссылка Stackblitz на код

person jcalz    schedule 23.04.2020
comment
Большое спасибо за такой подробный ответ, мне очень приятно. Это также немного объясняет - я действительно вижу, что просто работаю с ограничениями машинописного текста. - person Geoff Davids; 24.04.2020
comment
Была одна вещь, которую я до сих пор не понимаю: вы сказали, что причина, по которой функция фильтра не определяет результирующий тип, заключается в том, что пользовательские типы не распространяются. Но тот же результат получается при использовании !== undefined, который не является определяемым пользователем типом защиты: const o = new Observable<string | undefined>(); const a = o.pipe(filter(x => x !== undefined)); // a: Observable<string | undefined> - person Geoff Davids; 24.04.2020
comment
Конечно... результаты защиты типа никогда не распространяются автоматически за пределы области, в которой они используются. Это одна из причин, по которой вам в первую очередь нужны определяемые пользователем функции защиты типа (иначе любая функция, возвращающая x !== undefined, автоматически действовала бы как защита типа для x). ). Надеюсь, это имеет больше смысла. - person jcalz; 24.04.2020