2

I am defining an interface where one of the type of the property depends on a generic param P bound to an enum. I am using the following approach:

export enum Scopes {
  Fruit = 'fruit',
  Vegetables = 'vegetables',
}

export enum FruitItemTypes {
  Strawberry = 'strawberry',
  Rasberry = 'rasberry'
}

export enum VegetableItemTypes {
  Potatoes = 'potatoes',
  Carrots = 'currency',
}


export type ItemTypes = FruitItemTypes | VegetableItemTypes

interface ItemTypeForScope {
  [Scopes.Fruit]: FruitItemTypes;
  [Scopes.Vegetables]: VegetableItemTypes;
}

export interface Item {
  id: string;
  type: ItemTypes;
}
export interface ScopedItem<T extends Scopes> extends Item {
  type: ItemTypeForScope[T];
}
export interface ScopedData<T extends Scopes> {
  items: ScopedItem<T>[];
}

export type Data = { [scope in Scopes]: ScopedData<scope> };

I also want to use ScopedItem<T> as the return type of the following function:

const getItemType = <T extends Scopes>(data: Data, scope: T): ScopedItem<T>[] => {
    return data[scope].items 
}

However I am getting the following error, but according to me the generic param T will eventually be one of the enum case.

Type 'ScopedItem<Scopes.Fruit>[] | ScopedItem<Scopes.Vegetables>[]' is not assignable to type 'ScopedItem<T>[]'.
  Type 'ScopedItem<Scopes.Fruit>[]' is not assignable to type 'ScopedItem<T>[]'.
    Type 'ScopedItem<Scopes.Fruit>' is not assignable to type 'ScopedItem<T>'.
      Type 'Scopes.Fruit' is not assignable to type 'T'.
        'Scopes.Fruit' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Scopes'.

playground

vbvx
  • 609
  • 7
  • 22
  • About your playground link... those are not unions and intersections. You are using [logical operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators#Logical_AND_()) which are very different. `x || y` and `x && y` will always evaluate to one of `x` or `y` depending on the truthiness/falsiness of `x`. – jcalz Aug 05 '19 at 21:07
  • thanks for the detailed answer and the issue reference, I believe `Data[T]["items"]` is the same as the inferred type by the compiler. Type assertion seems like a good solution in that case. Regarding logical operators, it was a long day but thanks for the feedback regarding the irrelevance of it in this example :) – vbvx Aug 05 '19 at 21:54
  • [this answer](https://stackoverflow.com/a/59363875/8233039) is very detailed about this error message. Take a look on it. – Flavio Vilante Dec 18 '19 at 22:24

1 Answers1

3

I believe the problem here is the same one described in this issue... you want the compiler to evaluate {[K in Scopes]: ScopedData<K>}[P] to something like ScopedData[P], where P is a generic type parameter that extends K. But the compiler doesn't do this sort of higher-order reasoning where a generic function of a concrete type is simiplified before the generic type is resolved; there has been a suggestion to make this happen in some cases, but it's not there as of TS3.5.

So, workarounds... it is possible for the compiler to verify the following:

const getItemType = <T extends Scopes>(
  data: Data,
  scope: T
): Data[T]["items"] => {
  return data[scope].items;
};

Instead of returning the type of data[scope].items as ScopedItem<T>[], return it as Data[T]["items"]. Those will turn out to be the same thing, and when you actually call getItemType() on a concrete-typed scope parameter, it will end up as the same concrete type.


Or you can just admit that your reasoning skills are superior to those of the compiler, and use a type assertion to let the compiler know who's the boss:

const getItemTypeAssertion = <T extends Scopes>(
  data: Data,
  scope: T
): ScopedItem<T>[] => {
  return (data[scope] as ScopedData<T>).items; // I am smarter than the compiler 
};

Hopefully one of those will work for you. Good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360