Home > OS >  How to describe constraint using conditional type which is based on generic type?
How to describe constraint using conditional type which is based on generic type?

Time:04-19

I have a problem with typings in my code - union type constraint is generated instead of picking only one type from ItemProperties types dictionary. Is there any solution to make strict constraint for props property inside ItemConfig based on current ItemType?

P. S. wrapping T generic with tuple at props type declaration (as said here) not solves the problem.

Simplified code example:

enum ItemType {
  Dog = 1,
  Car = 2,
  Building = 3,
}

interface ItemProperties {
  [Item.Dog]: {
    name: string;
  };
  [Item.Car]: {
    power: number;
  };
}

interface ItemConfig<T extends ItemType = ItemType> {
  type: T;
  props: T extends keyof ItemProperties ? ItemProperties[T] : {};
}

const config: ItemConfig[] = [
  {
    type: ItemType.Dog,
    props: ...
  }
];

Expected typeof props:

  { name: string }

Actual typeof props:

  { name: string } | { power: number } | {}

CodePudding user response:

As remarked in the comments, the issue is that ItemConfig is equal to {type: ItemType, props: {} | {name: string} | {power: number}} whereas you'll want it to be a discriminating union {type: ItemType.Dog, props: {name: string}} | {type: ItemType.Car, props: {power: number}} | .. to type the array element props correctly.

One way to create this union is by using a distributive conditional type (docs):

type ItemConfig<T = ItemType> = T extends ItemType ? {
  type: T;
  props: T extends keyof ItemProperties ? ItemProperties[T] : {};
} : never

TypeScript playground

Because the T in the condition is a naked type parameter, ItemConfig<ItemType.Dog | ItemType.Car | ItemType.Building> distributes to ItemConfig<ItemType.Dog> | ItemConfig<ItemType.Car> | ItemConfig<ItemType.Building>, which is the desired union.

Alternatively (as captain-yossarian remarked), since ItemType extends PropertyKey (i.e. string | number | symbol), you can use a mapped type to create an object that has the constituents of the desired union as its values, and index that object to obtain the union:

type ItemConfig = {
  [T in ItemType]: {
    type: T,
    props: T extends keyof ItemProperties ? ItemProperties[T] : {}
  }
}[ItemType]

TypeScript playground

This has the advantage that you don't need ItemConfig to have a generic parameter T, but it is limited to types that extend PropertyKey (otherwise you can't use it as a key in the mapped type).

Both approaches approaches yield the same ItemConfig union, which will allow the appropriate props type to be inferred for each array element:

const config: ItemConfig[] = [
  {
    type: ItemType.Dog,
    props: {name: 'x'}
    // type of props: {name: string}
  },
  {
    type: ItemType.Car,
    props: {power: 7}
    // type of props: {power: number}
  },
  {
    type: ItemType.Dog,
    props: {power: 7} // Type error
    // type of props: {name: string}
  }
];
  • Related