One limitation of TypeScript is the lack of support for "nested" discriminated unions, where a subproperty is used to discriminate among members. For more information on this issue, refer to microsoft/TypeScript#18758. While type C
is considered a discriminated union with name
as the discriminant, attempting to use type.name
as the discriminant will not work as expected. This eliminates the ability to utilize features designed to handle discriminated unions, such as using a switch
statement based on the discriminant property.
If you need the compiler to perform type guarding on subproperties without altering your object structure or resorting to type assertions, one possible solution is to implement a user-defined type guard function. An example of how this can be achieved:
function hasNestedTypeName<
T extends { type: { name: string } },
K extends string
>(t: T, k: K): t is
T extends { type: { name: infer N } }
? K extends N ? T : never
: never {
return t.type.name === k
}
By calling hasNestedTypeName(t, k)
and receiving a true
result, the compiler will narrow down t
to only those union members whose type.name
property matches the provided value k
. It's necessary to refactor the test
function to use if
/else
conditions instead of a switch
due to the boolean return type of type guard functions. Here is an updated version:
function test(t: C) {
if (hasNestedTypeName(t, "a")) {
// function hasNestedTypeName<C, "a">(t: C, k: "a"): t is A
t // A
console.log(t.a)
} else if (hasNestedTypeName(t, "c")) {
// function hasNestedTypeName<B | Generic, "c">(t: B | Generic, k: "c"): t is Generic
t // Generic
console.log(t.name)
} else if (hasNestedTypeName(t, "b")) {
t // B
console.log(t.b)
}
This approach offers similar functionality to your previous code, although additional care must be taken when using
hasNestedTypeName(t, "c")
to ensure it behaves correctly. It's advisable to avoid nested discriminated unions with union-based discriminants, as it may lead to unpredictable behavior. Ultimately, whether implementing such changes is worthwhile depends on the specific use case. Refactoring the object structure to have the discriminant at the top level might be a more effective strategy, leveraging TypeScript's strengths rather than trying to work around its limitations.
For a live demonstration of the code in action, check out this Playground link.