'TypeScript: Conditional Intersection via Discriminated Union

I'm try to efficiently add properties to an object type based on one of the existing property values. For example, there is a default type BaseField shared between many types of fields. The type of the field comes from an enum. For example:

enum FieldType {
  Currency = 'currency',
  Text = 'text',
}

type BaseField<T extends FieldType = FieldType> = {
  id: string;
  type: T;
}

Some fields have additional properties depending on their type. For example, a field with type Currency also has a defaultCurrency property. I originally tried to use a map and a discriminated union to achieve this, something like:

type CurrencyField = {
  defaultCurrency: string;
}

type FieldMap = {
  [FieldType.Currency]: CurrencyField,
  [FieldType.Text]: never,
}

type Field<T extends FieldType = FieldType> = T extends T 
  ? BaseField<T> & FieldMap[T] 
  : never;

This half works. If I create a field as such:

const field: Field = {
  id: 'field1',
  type: FieldType.Currency,
}

I correctly get a type error Property 'defaultCurrency' is missing in type. And, if I add it in, the type is valid. However, the issue arises when I use one of the types where the FieldMap has the value of never. If I try to make a text field:

const field: Field = {
  id: 'field2',
  type: FieldType.Text,
}

I see the error Type 'FieldType.Text' is not assignable to type 'FieldType.Currency'.. I have tried a few ways to get around this:

  1. Instead of never use BaseField as the type in the map
  2. Remove the params from BaseField type
  3. Modify the union to BaseField<T> & T extends T ? FieldMap[T] : never;

However, I am unable to resolve this. I can get it to work by manually creating the fields and joining them together:

type CurrencyField = {
  id: string;
  defaultCurrency: string;
  type: FieldType.Currency;
}

type TextField = {
  id: string;
  type: FieldType.Text;
}

type Field = CurrencyField | TextField;

However, I'd like to keep this as scalable as possible and would rather not have to create new types every time a new field was added. Is there a way to achieve this? Thank you for your help!



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source