1
votes

I have an abstract class with default state values that are typed inside ResourceState interface. I am getting a following error when I try to assign my extended State to the state property of my abstract class. I tried many different ways using intersection, though it still doesn't work. What I would like to achieve is, that I can provide a generic interface to the state property so that I can add more values apart from the default ones.

This is the error I got:

Type '{ items: never[]; isLoading: false; error: string; }' is not assignable to type 'State'. '{ items: never[]; isLoading: false; error: string; }' is assignable to the constraint of type 'State', but 'State' could be instantiated with a different subtype of constraint 'ResourceState'.

This is my code:

export interface BaseItem {
  id: string;
  name: string;
}

export interface ResourceState < Item extends BaseItem > {
  items: Item[];
  selectedItem ? : Item;
  isLoading: boolean;
  error: string;
}

export abstract class Resource < Item extends BaseItem, State extends ResourceState < Item > , Mutations, Getters, Actions > {
  state: State = { // this is where the error occurs
    items: [],
    isLoading: false,
    error: '',
  }
}
1

1 Answers

2
votes

When you say State extends ResourceState<Item> you are saying that State needs to conform to ResourceState<Item> but because this is a generic type parameter, it may not be exactly a ResourceState<Item>.

Imagine this:

interface MyState extends ResourceState<{ a: 123 }> {
  myCustomRequiredProperty: number
}

Now you pass MyState as State to your class, then you contruct state with:

  state: State = { // this is where the error occurs
    items: [],
    isLoading: false,
    error: '',
  }

Then your state variable would not be constructed with the myCustomRequiredProperty property, which is required.

This is the error that typescript is (cryptically) trying telling you.


If you are constructing the state variable in the Resource class, then the State should not be generic because your executable code in this class can't know all properties of the types that could be generically passed in. So State should not extend ResourceState<Item>, it should be exactly a ResourceState<Item>.

That would look like this:

export abstract class Resource<Item extends BaseItem> {
  state: ResourceState<Item> = {
    items: [],
    isLoading: false,
    error: '',
  }
}

Playground


However, if you want the ability to add properties to state, then you can't do that without some initial values. You need to somehow initialize those unknown properties. To do that, you would use your approach above, but with a constructor that accepts whatever type is the full state and then fill in the unknown properties of that type with an object that has that info.

export abstract class Resource<
  Item extends BaseItem,
  State extends ResourceState<Item>
> {
  state: State

  constructor(initialState: State) {
    this.state = {
      ...initialState, // Fills in extended properties
      items: [],
      isLoading: false,
      error: '',
    }
  }
}

Playground