Trying to reach peak TypeScript performance I'm currently getting into some of the niche areas of the language, and there's something I don't get.
With strict-null-checks and strict mode and that stuff enabled, I don't understand why this code gives me an error:
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;
}
Clearly, even if T
is a type like string | number | undefined
, by the time I'm trying to push the filtered value into the array, it's pretty damn clear that it can't be undefined
. Is TypeScript not smart enough to follow me here (I don't think so, because similar things work just fine), am I missing a crucial detail, or is the NonNullable
type simply not as powerful as one might expect?
(I'm aware that there are more concise methods of doing what I'm doing here, however I need code with a similar logic to do more complicated things, so the Array.prototype.filter
style method is not quite applicable)
-------Edit:
I kind of get the problem now, however, instead of using type assertions in the push
method, I'd then rather rewrite the whole thing to something like
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;
}
which seems to do a better job. But it needs more writing.
What I'm really trying to do is find a better way to write a utility for NGRX, as this is really annoying (thank god there isn't a third nullish type)
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());
}
I'd hope to find a way to write this like
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>'.
}
which, unfortunately, does not work