I've encountered strange behavior when using complex nested generics.
Here is the distilled version of my real-life problem:
type Variable = 'A' | 'B';
type Option<T extends Variable> = T extends any ? { value: T } : never;
type OptionAny = Option<Variable>;
type Generic<T extends Variable> = Extract<OptionAny, { value: T }>;
// ==============================================================================
// JUST IN CASE, IF YOU'RE CURIOUS, THIS IS FINE:
type GenericHandler<T extends Generic<Variable>> = { handle(input: T): void };
// no error
type GenericHandlerA = GenericHandler<Generic<'A'>>;
// ==============================================================================
type NestedGeneric<T extends Variable> = { generic: Generic<T> };
type NestedGenericHandler<T extends NestedGeneric<Variable>> = {
handle(input: T): void;
};
// error
// TS2344: Type NestedGeneric<'A'> does not satisfy the constraint NestedGeneric<Variable>
// Type Variable is not assignable to type 'A'
// Type 'B' is not assignable to type 'A'
type NestedGenericHandlerA = NestedGenericHandler<NestedGeneric<'A'>>;
// -----------------------------------------------------------------------------
// the expected solution (to my understanding of TS):
// use distributive conditional type on type, that is nesting another generic
// -----------------------------------------------------------------------------
// distribute over possibly union T
type NestedGenericDistributive<T extends Variable> = T extends any
? { generic: Generic<T> }
: null;
type NestedGenericDistributiveHandler<
T extends NestedGenericDistributive<Variable>
> = {
handle(input: T): void;
};
// ok
type NestedGenericDistributiveHandlerA = NestedGenericDistributiveHandler<
NestedGenericDistributive<'A'>
>;
// ok, too
type NestedGenericDistributiveHandlerA2 = NestedGenericDistributiveHandler<
NestedGeneric<'A'>
>;
// because:
type TestDistributive = NestedGenericDistributive<Variable>;
// ^? {generic: Generic<"A">} | {generic: Generic<"B">}
type TestNonDistributive = NestedGeneric<Variable>;
// ^? {generic: Generic<"A" | "B">}
// -----------------------------------------------------------------------------
// Now, the bloody black magic:
// Use type aliases for either the constraint or concrete NestedGeneric type
// -----------------------------------------------------------------------------
type NestedGenericAliasAny = NestedGeneric<Variable>;
type NestedGenericAliasHandler<T extends NestedGenericAliasAny> = {
handle(input: T): void;
};
// no error
type NestedGenericAliasHandlerA = NestedGenericAliasHandler<NestedGeneric<'A'>>;
// -----------------------------------------------------------------------------
type NestedGenericAAlias = NestedGeneric<'A'>;
// no error
type NestedGenericHandlerAAlias = NestedGenericHandler<NestedGenericAAlias>;
// -----------------------------------------------------------------------------
// More bloody magic...
// Replacing `Generic` with:
// -----------------------------------------------------------------------------
// doesn't help
type GenericDistributiveBad<T extends Variable> = T extends any
? Extract<OptionAny, { value: T }>
: null;
// even though
type TestBad = GenericDistributiveBad<Variable>;
// ^? | Extract<{value: "A"} | {value: "B"}, {value: "A"}>
// | Extract<{value: "A"} | {value: "B"}, {value: "B"}>
// But this does help
type GenericDistributiveGood<T extends Variable> =
| (T extends 'A' ? { value: T } : never)
| (T extends 'B' ? { value: T } : never);
// Is this different to `TestBad`?
type TestGood = GenericDistributiveGood<Variable>;
// ^? {value: "A"} | {value: "B"}
// afterall, `Extract` is
// `type Extract<T, U> = T extends U ? T : never;`
// which means that each type in the TestBad union must go through the following:
// Extract<{value: "A"} | {value: "B"}, {value: "A"}>
// is equal to
// | ({value: "A"} extends {value: "A"} ? {value: "A"} : never)
// | ({value: "B"} extends {value: "A"} ? {value: "B"} : never)
// same for Extract<{value: "A"} | {value: "B"}, {value: "B"}>
// ...
// Result is: {value: "A"} | {value: "B"}
// -----------------------------------------------------------------------------
// Oh, and BTW
// -----------------------------------------------------------------------------
type TestGeneric = Generic<'A' | 'B'>;
// ^? {value: "A"} | {value: "B"}
// :)
Can anybody explain strictly how it works and why?
(Goodluck answering this with ChatGPT)
To my understanding typescript should just try to check if NestedGeneric<'A'>
is assignable to constraint NestedGeneric<Variable>
While doing it, it should check if type parameter T
in NestedType
is invariant or covariant (correct me if I am messing up the terminology). If the NestedType is a distributive conditional type over T, the T is covariant, otherwise, by default, it is invariant. If it is invariant, and the given type (of T) is not exactly the same as the constraint, ts throws an error.
Otherwise, it goes deeper.
Therefore
type NestedGenericHandlerA = NestedGenericHandler<NestedGeneric<'A'>>;
raises TS error, and the "expected solution" works
type NestedGenericDistributive<T extends Variable> = T extends any
? { generic: Generic<T> }
: null;
this is covariant case and everything is ok.
So, the first question is - is the above explanation correct?
Now, if it is correct, then what is going on in the code that goes below the "expected solution"?