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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With