How to get a discriminated union of a field value from an array of class instances?

Viewed 43

Problem

I would like to create a type to set up a type to get the union of all possible constant of a field values for a given array of class instances.

Example

Let's use this code to illustrate what I need.

specific.columns.config.ts
export const SPECIFIC_COLUMNS_CONFIG = [
  new ColumnConfig(
    {
      field: NAME_FIELD,
      isEditable: true,
      ...
    }
  ), 
  new ColumnConfig(
    {
      field: STATUS_FIELD,
      isEditable: false,
      ...
    }
  ),
  ... 
] as const;
column.config.ts
class ColumnConfig {
  field: string;
  isEditable: boolean = false;
  ...

  constructor(partial: Partial<ColumnConfig>){
    Object.assign(this, partial)
  }
}
field.config.ts
export const NAME_FIELD = "name" as const; // these fields can be used in multiple column configs
export const STATUS_FIELD = "status" as const;
...

Here is what I want to have:

Expected result
type SpecificFields = NAME_FIELD | STATUS_FIELD | ...

What I've tried

I've found a great blog article: https://steveholgado.com/typescript-types-from-arrays/

But unfortunately, it works with raw object (and not class instances):

type SpecificFields = typeof SPECIFIC_COLUMNS_CONFIG[number]["field"] // type SpecificFields = string;

An alternative solution I found is to hard-code all the different values in a constant likewise and extends ColumnConfig:

const SPECIFIC_FIELDS = [ NAME_FIELD, STATUS_FIELD, ... ]

class SpecificColumnConfig extends ColumnConfig {
   field: typeof SPECIFIC_FIELDS;

  constructor(partial: Partial<SpecificColumnConfig>) {
    super(partial);
  }
}

But the disadvantage with this solution is that we have to manually add the fields every time we define a new ColumnConfig and I wonder if there was a more effortless solution.

Thanks in advance for your answers !

1 Answers

If I understand you correctly

export class ColumnConfig<Field extends string> {
  public field: Field
  public isEditable: boolean = false

  constructor(partial: Partial<ColumnConfig<Field>> & Pick<ColumnConfig<Field>, "field">) {
    this.field = partial.field
    this.isEditable = partial.isEditable ?? false
  }
}

const SPECIFIC_COLUMNS_CONFIG = [
  new ColumnConfig({
    field: "name",
    isEditable: true,
  }),
  new ColumnConfig({
    field: "status",
    isEditable: false,
  }),
] as const

// inferred type of ConfiguredColumns is "name" | "status"
type ConfiguredColumns = typeof SPECIFIC_COLUMNS_CONFIG[number] extends ColumnConfig<infer Name> 
  ? Name
  : never

Explanation: Generics in ts can hold any data, and we just store that data (unique field name) in generic Field on ColumnConfig and gets [ ColumnConfig<"name">, ColumnConfig<"status"> ], then, we extract field name using infer ts keyword (infer in this case using for extract generic parameter) and gets [ "name", "status" ] (ColumnConfig<infer R> means: try to get R (Field in ColumnConfig) from generic) and then we construct union using ternary operator on types.

Related