Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Object discriminated union types are merged when using fallback

I am defining an array that needs to accept types of string, and object. The object types must contain two properties: name and value. value must be another object, containing an arbitrary set of key: value pairs.

I am trying to define some of the object types using a discriminated union, so that the properties of value for some specific name are known. However, there always needs to remain a fallback so that when name is not a known string literal, value can still be any arbitrary set.

Here is what I'm working with so far:

interface IFallbackDef {
    name: string;
    value: object;
}
type ValueDef<TName extends string = string, TOptions extends object = {}> = {
    name: TName;
    value: TOptions;
};
type Merged<TValueDef extends ValueDef> = (string | TValueDef | IFallbackDef)[];


interface ITest1Options {
    foo: string;
    bar: string;
}
interface ITest2Options {
    baz: string;
    qux: string;
}

const test: Merged<
    | ValueDef<'test1', ITest1Options>
    | ValueDef<'test2', ITest2Options>
> = [
    'asdf',
    {
        name: 'test1',
        value: {
            foo: 'asdjfkl',
            bar: 'asdf',

            /**
             * Intellisense shows both sets of properties,
             * and typescript allows them all, too
             */
            qux: 'asdfkljsdg' // This should be an error
        }
    },
    {
        name: 'test2',
        value: {
            baz: 'blah',
            qux: 'test',

            /**
             * Intellisense shows both sets of properties,
             * and typescript allows them all, too
             */
            foo: 'salfdj' // This should be an error
        }
    },
    {
        name: 'asdf',
        value: {
            /**
             * Intellisense shows both sets of properties,
             * should show none.
             */
        },
    },
]

The issue I have is that when I include IFallbackDef in the union, all of the types for the various value properties merge. If I exclude IFallbackDef, the union works correctly, but the last index of the test array will be an error, because name: 'asdf' is unknown.

I assume that because IFallbackDef uses base types, and the shape is the same as IValueDef, it is merging the types... ? At the moment, I'm kind of at a loss for how to make this work properly. A fresh set of eyes would be very appreciated.

like image 246
Jeremy Albright Avatar asked Sep 01 '25 04:09

Jeremy Albright


1 Answers

If you expand out the Merged type you end up with a type like:

(
    | string
    | { name: "test1", value: { foo: string, bar: string } }
    | { name: "test2", value: { baz: string, qux: string } }
    | { name: string, value: object }
)[]

The problem is that that isn't a discriminated union because of the | { name: string, value: object }. That line breaks discriminated union checking because string is a match for both 'test1' and 'test2', so TypeScript cannot discriminate.

You need to remove that line (e.g., remove IFallbackDef as you suggested) to get discriminated unions working. Unfortunately, I don't believe there is a workaround that will let you achieve the goal of having things with a specific key type be of a specific value type, but then having things without one of those specific key types be of a different value type.

Depending on your constraints, one option would be to use a different key for the fallback. For example:

interface IFallbackDef {
    not-name: string;
    value: object;
}

This will allow for discriminated union type checking of the objects with a name key separate from objects with a not-name key.

like image 127
Micah Zoltu Avatar answered Sep 02 '25 19:09

Micah Zoltu