Search code examples
typescriptvuejs3pinia

'T' could be instantiated with an arbitrary type which could be unrelated to 'T | null | undefined'


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.


Solution

  • 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.

    Playground link to code