Search code examples
typescripttypesenumsconditional-typesmapped-types

Why is the Exclude utility type not working as expected in combination with mapped conditional types on enums in TypeScript?


Just to give a little bit of context: I'm building a quite complicated form with conditional required values that depend on what has been selected in previous inputs.

Therefore, I made the following object that tells whether an input should be required or not:

enum IssueType {
  Error = "Error",
  Advice = "Advice",
}

enum Category {
  Software = "Software",
  Hardware = "Hardware",
  Other = "Other",
}

const inputNames = {
  issueType: "issueType",
  concerning: "concerning",
  serialNumber: "serialNumber",
  errorCode: "errorCode",
} as const;

const conditions: Conditions = {
  [IssueType.Error]: {
   ~~~~~~~~~~~~~~~> Property 'Other' is missing in type ...
    [Category.Software]: {
      [inputNames.concerning]: true,
      [inputNames.serialNumber]: true,
      [inputNames.errorCode]: true,
    },
    [Category.Hardware]: {
      [inputNames.concerning]: true,
      [inputNames.serialNumber]: true,
      [inputNames.errorCode]: true,
    }
  },
  [IssueType.Advice]: {
    [Category.Software]: {
      [inputNames.concerning]: true,
      [inputNames.serialNumber]: true,
      [inputNames.errorCode]: true,
    },
    [Category.Hardware]: {
      [inputNames.concerning]: true,
      [inputNames.serialNumber]: true,
      [inputNames.errorCode]: true,
    },
    [Category.Other]: {
      [inputNames.concerning]: true,
      [inputNames.serialNumber]: true,
      [inputNames.errorCode]: true,
    }
  }
};

Now my question is what would the type (Conditions in the snippet above) of conditions look like. I have tried this:

type Conditions = {
  [K in IssueType]: {
    [K in IssueType extends IssueType.Error
      ? Exclude<Category, Category.Other>
      : Category]: {
      [K in Exclude<
        keyof typeof inputNames,
        typeof inputNames.issueType
      >]: boolean;
    };
  };
};

but the Exclude of Category.Other doesn't work. It should only be a mandatory property if it's parent's object key is of type IssueType.Error. What am I doing wrong here?

Also I want to avoid (if possible) Nullable Types like that for instance:

type Conditions = {
  [K in IssueType]: {
    [K in Category]?: {
      [K in Exclude<
        keyof typeof inputNames,
        typeof inputNames.issueType
      >]: boolean;
    };
  };
};

Solution

  • In the second level of the object, you should reference K (from one level up) instead of IssueType. To do this, you cannot name the key type with K for all levels.

    Here's one where I renamed K's with A, B, and C for clarity:

    type Conditions = {
      [A in IssueType]: {
        [B in A extends IssueType.Error
          ? Exclude<Category, Category.Other>
          : Category]: {
          [C in Exclude<
            keyof typeof inputNames,
            typeof inputNames.issueType
          >]: boolean;
        };
      };
    };
    

    Check the playground.