There are a number of these questions on SO, but I still can't seem to find one that helps me figure out what's going on here.
Context, I'm using Pinia (VueJS), and I'm creating a "global" store, for a whole bunch of common actions/state that exist across many modules.
import find from 'lodash/find';
export interface IGenericPaginationStore<T> {
list: T[];
count: number;
page: number;
next: string | null;
previous: string | null;
active: T | null;
pageSize: number;
}
// store.ts
setActive<T extends { uuid: string }>(state: IGenericPaginationStore<T>, args: { uuid?: string | null, resource?: T | null }) {
if (args.resource) {
state.active = args.resource
state.list.forEach((resource: T, index) => {
if (resource?.uuid === args.resource?.uuid) {
state.list[index] = args.resource // **** ERROR 1 HERE ****
}
})
return;
}
},
Error 1:
TS2322: Type 'T | null | undefined' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'T | null | undefined'.
I just can't figure out why this doesn't work. Any help would be greatly appreciated.
The problem you're having is that the effects of checking args.resource
for truthiness does not persist into the scope of the body of the forEach()
callback.
This is a general limitation of TypeScript's control flow analysis; it essentially is unable to cross function boundaries. See microsoft/TypeScript#9998 for a full discussion of why this is a hard problem to solve, and why the behavior is the way it is. In short, the compiler cannot easily be sure that args.resource
will not get reassigned before the forEach()
callback is called. We know that forEach()
will run immediately, but the compiler cannot know this. (See microsoft/TypeScript#11498 for a feature request to allow a way to say that a callback will be run immediately.)
So the narrowing of args.resource
from T | undefined | null
to T
is undone inside the callback. And the compiler complains that you can't assign a value of type T | undefined | null
to a property of type T
.
In cases like this, the workaround is usually to create a new const
with the value you want to type guard. A const
cannot be reassigned, and the compiler knows this. So if you check that a const
is not null
or undefined
, then that const
cannot be null
or undefined
in all scopes, even inside some callback:
const resource = args.resource; // copy the value here
if (resource) {
// resource is T here
state.active = resource
state.list.forEach((r: T, index) => {
// resource is still T here
if (r.uuid === resource.uuid) {
state.list[index] = resource; // okay
}
})
return;
}
Now the compiler is happy, since resource
is seen as being type T
inside the callback, and thus can be assigned to the property of type T
as well.