How do I assign object to generically typed property of a reactive proxy?

Viewed 34

I'm trying to create a class I can use to load data asynchronously. Instances of this class are used in a Pinia store and watched by Vue components to enable automatic UI updates when data is loaded.

However, I get a compile-time error:

Type 'T' is not assignable to type 'UnwrapRef<T> | undefined'

Here's the code:

export class AsyncData<T> {
    state: AsyncDataState
    data?: T

    constructor(request?: Promise<T>) {
        if (request) {
            this.state = AsyncDataState.Loading

            // use reactive proxy to modify state later in case anyone is listening
            const reactiveThis = reactive(this)

            request.then(d => {
                // got it
                reactiveThis.state = AsyncDataState.Loaded
                reactiveThis.data = d // <= ERROR
            },
            reason => {
                // no dice
                reactiveThis.state = (reason == DataError.NotFound ? AsyncDataState.NotFound : AsyncDataState.LoadFailed)
            })
        }
        else {
            this.state = AsyncDataState.New
        }
    }
}

I'm not an expert on the mysteries of Typescript or Vue 3, but it seems to me I should be able to assign a value of type T to the property on a reactive wrapper corresponding to a property of type T.

What am I missing?

1 Answers

reactive is designed for plain object, so it will lose the type definition.

And the reactiveThis in your constructor wouldn't work, because the constructor returns this not reactiveThis. So nobody can "listen" this reactive variable.

Here is my optimized version:

import { ref } from 'vue';
import type { Ref } from 'vue';

export class AsyncData<T> {
  state: Ref<AsyncDataState> = ref(AsyncDataState.New)
  data?: T

  constructor(request?: Promise<T>) {
      if (request) {
          this.state.value = AsyncDataState.Loading

          request.then(d => {
              // Notice: Make sure the assignments of this.data always before this.state
              // because when this.state changes, effect triggers immediately
              this.data = d
              this.state.value = AsyncDataState.Loaded
          },
          reason => {
              // no dice
              this.state.value = (reason == DataError.NotFound ? AsyncDataState.NotFound : AsyncDataState.LoadFailed)
          })
      }
      else {
          this.state.value = AsyncDataState.New
      }
  }
}

Only the state is reactive, and data keep the type definition.Then it can be used as below:

import { effect } from 'vue';

const p = fakePromise<Cat>();
const data = new AsyncData<Cat>(p);

effect(() => {
  if (data.state.value === AsyncDataState.Loaded) {
    console.log('receive new data', data.data);
  } else {
    console.log('check data', data.data);
  }
});
Related