typescriptvalidationtypes

Throwing an IDE error from validating an object


I have a task to build a custom radio input component in React. My idea was that the options for those radios would be assigned as an object and then mapped in the following way:

type OptionLiteral = {
  label: string;
  value: string;
  selected?: true;
};

const options = [
  { label: "A", value: "a", selected: true },
  { label: "B", value: "b" },
  { label: "C", value: "c" },
];

I thought it would be smart to also implement a check for the amount of options set as 'selected' that would throw an error directly into their IDE (with a red wavy line, and so on), because in a radio component there is only one selectable option. This is meant to help future developers deal with issues that may arise from implementing this radio input component on their pages.

To do this, I have built a dummy type checker that I would implement into the component later:

type Option = {
  label: string;
  value: string;
  selected?: true;
};

type IsSelected<T> = T extends { selected: true } ? 1 : 0;

type CountSelected<T extends readonly any[], Acc extends any[] = []> =
  T extends [infer Head, ...infer Tail]
    ? CountSelected<Tail, [...Acc, ...IsSelected<Head>[]]>
    : Acc["length"];

type ValidateSingleSelected<T extends readonly Option[]> =
  CountSelected<T> extends 0 | 1
    ? T
    : ["❌ Error: More than one item has selected: true."];

type RadioOptions<T extends readonly Option[]> = ValidateSingleSelected<T>;

const options = [
  { label: "A", value: "a", selected: true },
  { label: "B", value: "b", selected: true }, // returns no error
  { label: "C", value: "c" },
] as const;

type _ = RadioOptions<typeof options>; 

However, no matter what I do, this code does not seem to work: it either invalidates the input, even if there is one or none of the options with selected: true; or it doesn't invalidate the input at all, even when it is meant to be wrong.


Solution

  • It appears that the type would not get initialised in the IDE compiler unless it is used within a function, which means it would not return an instant type error. To work around this, I have used the answer from French Croissant and added in a dummy enfornceSingleSelected function that processes the type, and here is the final solution:

    type Option = {
      label: string;
      value: string;
      selected?: true;
    };
    
    type IsSelected<T> = T extends { selected: true } ? [1] : [];
    
    type CountSelected<
      T extends readonly any[],
      Acc extends any[] = []
    > = T extends [infer Head, ...infer Tail]
      ? CountSelected<Tail, [...Acc, ...IsSelected<Head>]>
      : Acc;
    
    type ValidateSingleSelected<T extends readonly Option[]> =
      CountSelected<T>["length"] extends 0 | 1
        ? T
        : ["❌ Initialisation Error: More than one item has 'selected: true'"];
    
    type RadioOptions<T extends readonly Option[]> = ValidateSingleSelected<T>;
    
    function enforceSingleSelected<T extends readonly Option[]>(
      options: RadioOptions<T>
    ): T {
      return options as T;
    }
    
    const options = enforceSingleSelected([
      { label: "A", value: "a", selected: true }, // Error appears here as a type error
      { label: "B", value: "b", selected: true }, 
      { label: "C", value: "c" },
    ] as const);