All to make a conditional type with all possible generics?

I have this types:

type IInputType = "text" | "checkbox" | "number";

type IField<T extends IInputType> = {
  name: string;
  value: 
   T extends "checkbox" ? boolean : 
   T extends "number" ? number : 
   string;
  type: T;
};

type IPossibleFieldTypes = IField<IInputType>;

That way the conditional is not working because field.value is typed as string, number or boolean, and not only boolean as should:

 // field type is IPossibleFieldTypes 
 if (field.type === "checkbox") {
  // Here field.value should only be boolean
  // but TS are allowing string or number too
 }

But if I specify the possible IFields manually:

type IPossibleFieldTypes =
  | IField<"text">
  | IField<"checkbox">
  | IField<"number">;

Now the types are being displayed correctly:

 if (field.type === "checkbox") {
  // Here field.value can only be boolean
 }

The question is:

There is a way to make this type work (IPossibleFieldTypes) without have to manually set the possible generics?

>Solution :

Make IField a distributive conditional type, so that you make a discriminated union:

type IInputType = keyof ValueMap;

type ValueMap = {
    text: string;
    checkbox: boolean;
    number: number;
};

type IField<T extends IInputType> = T extends T ? {
    name: string;
    value: ValueMap[T];
    type: T;
} : never;

Also, we take advantage of the fact that T is a string, which means instead of a long chain of conditional types, we can use another type that maps T to the right type. Then narrowing will work correctly:

if (field.type === "checkbox") {
    field.value;
    //    ^? boolean
}

Playground

Leave a Reply