Home > Enterprise >  TypeScript: Limit discriminator in discriminated union from other string literal union type
TypeScript: Limit discriminator in discriminated union from other string literal union type

Time:03-03

I have a very basic discriminated union, but for the discriminator, I only want to allow a specific values that are coming from another string literal union type. This would make it easier to add new union "cases" to the discriminated union.

Here's a string literal union which describes the allowed "values" for the type:

type AllowedType = "typeForNumber" | "typeForBoolean"

Then, my type to describe the data uses that string literal union:

type Data = {
  type: AllowedType,
  value: number | boolean
}

Based on my example, there are two options, which can have a more specific type for value based on their type field. I don't use these options, they're just here for demonstration purposes:

// Option 1 - "value" is a "number"
type DataOption1 = {
  type: "typeForNumber",
  value: number
}
// Option 2 - "value" is a "boolean"
type DataOption2 = {
  type: "typeForBoolean",
  value: boolean
}

So what I actually want to do is a discriminated union for Data, because I can give more specific types for its value field that way:

type Data =
  | {
      type: "typeForNumber"
      value: number
    }
  | {
      type: "typeForBoolean"
      value: boolean
    }

When you use the type, everything works fine:

const myData: Data = {
  type: "typeForNumber",
  // this creates an error, should be a `number`
  value: "some string"
}

My question is: How do I make sure that the type field in my Data type can only be one of the options of the AllowedType?

In the future, there will be more options for AllowedType, so I want to limit the possible union types with them.

One could change add another union to the Data type like so, without any error:

type Data =
  | {
      type: "typeForNumber"
      value: number
    }
  | {
      type: "typeForBoolean"
      value: boolean
    }
  | {
      type: "someOtherType"
      value: string
    }

This new union (with type: "someOtherType") shouldn't be allowed.

Is it possible to limit the discriminator (type) in this discriminated union (Data) from other string literal union type (AllowedType)?

I tried to use a wrapper for the intersection, but the unions ignore (overwrite?) the type type:

type AllowedType = "typeForNumber" | "typeForBoolean"

type DataWrapper = {
  type: AllowedType
  value: number | boolean
}

type Data = DataWrapper &
  (
    | {
        type: "typeForNumber"
        value: number
      }
    | {
        type: "typeForBoolean"
        value: boolean
      }
  )

CodePudding user response:

If I'm understanding correctly, the issue is Data and AllowedType possibly getting out of sync. If that's the issue, you could turn things around and define AllowedType in terms of Data, like this:

type Data =
  | {
      type: "typeForNumber"
      value: number
    }
  | {
      type: "typeForBoolean"
      value: boolean
    };

type AllowedType = Data["type"];

Playground link

Then, adding to Data automatically adds to AllowedType.


In a comment you've said you're trying to protect against typos in the type of the Data union:

In my case, the Data type represents dynamic data coming from another source, so I want to be sure that there are e.g. no typos in the type values (like typeForNumber). That's why I'm so keen to find a solution to limit the type to specific values. Turning it around doesn't help in my case.

This seems to help with that:

type CheckData<DataType extends {type: AllowedType}> =
    Exclude<DataType["type"], AllowedType> extends never
        ? DataType
        : never;

Then this works:

type Data = CheckData<
    | {
        type: "typeForNumber"
        value: number
    }
    | {
        type: "typeForBoolean"
        value: boolean
    }>;

but this doesn't:

type Data2 = CheckData<
    | {
        type: "typeForNumber"
        value: number
    }
    | {
        type: "typeForBoolean"
        value: boolean
    }
    | {
        type: "typeForSomethingElse"
        value: boolean
    }>;

Playground link

It doesn't catch all typos, though. It allows two types that both use the same type value.

  • Related