To get a thorough response, refer to this FAQ entry.
The foundation of TypeScript's type system is primarily structural rather than nominal. If two types A
and B
exhibit the same structure, meaning they have identical property names with matching property types, TypeScript considers them the same type. The compiler treats A
and
B</code as fully interchangeable, even if they have distinct <em>names</em> or are declared at different sites.</p>
<p>Consider your <code>Base
class:
abstract class Base<
TObject extends object,
TSecondaryObject extends SecondaryObjectConstraint
> { }
The structural composition of this class is... void. An instantiation of Base<X, Y>
possesses no identifiable properties whatsoever. Consequently, under structural typing rules, it equates to the null object type {}
without any reliance on the TObject
or
TSecondaryObject</code type parameters.</p>
<p>This scenario implies that <code>Base<{}, SecondaryObjectType>
and
Base<object, SecondaryObjectConstraint>
align structurally, making them substitutable for each other by the compiler. Hence, there's uncertainty regarding inferring
X
and
Y</code from the <code>Base<X, Y>
type. All you are assured of is receiving types consistent with their prescribed
restrictions.
Although feasible for the compiler to return Y
from
Base<X, Y> extends Base<X, infer T> ? T : never
, it's not guaranteed. Unfortunately, incorporating an intermediate
ExtendedObject
class generates the constraint type instead of the specific type intended.
To ensure more certainty, incorporate a structural dependency between your types. Generic types should actively utilize their type parameters in some structural fashion. For illustration:
abstract class Base<T extends object, U extends SecondaryObjectConstraint> {
t!: T;
u!: U
}
Here, a Base<T, U>
entails a t
property of type T
alongside a u
property of type U
. This setup yields the desired outcome:
type IOEO = InferSecondaryObject<ExtendedObject>;
/* type IOEO = {
myProp: number;
} */
Code Playground Link to Experimentation