Home > Net >  How to infer the correct generic type in this case?
How to infer the correct generic type in this case?

Time:07-03

Consider the following code:

type secondaryObjectConstraint = {
    [key: string]: number
}

abstract class Base<TObject extends object, TSecondaryObject extends secondaryObjectConstraint> {}


type secondaryObjectType = {
    myProp: number
}

class ExtendedObject extends Base<{}, secondaryObjectType> {}

type inferSecondaryObject<M extends Base<any, any>> = M extends Base<any, infer TSecondaryObject> ? TSecondaryObject : never;

const a: inferSecondaryObject<ExtendedObject> = {};

In the above example, a gets the type of secondaryObjectConstraint, instead of the more exact secondaryObjectType. What am I doing wrong here? How could I infer the secondaryObjectType in my above example?

Link to playground

CodePudding user response:

See this FAQ entry for an authoritative answer.

TypeScript's type system is largely structural and not nominal. If two types A and B have the same structure (e.g., both of them have the same property names of the same property types), then TypeScript considers them to be the same type. The compiler is free to treat A and B completely interchangeably. This is true even if A and B have different names or declaration sites.

Let's look at your Base class:

abstract class Base<
  TObject extends object, 
  TSecondaryObject extends SecondaryObjectConstraint
> { }

The structure of that class is... empty. An instance of Base<X, Y> has no known properties at all. And therefore, by structural typing, it is identical to the empty object type {}. It has no structural dependency on the TObject or TSecondaryObject type parameters.

This also implies that Base<{}, SecondaryObjectType> and Base<object, SecondaryObjectConstraint> are structurally identical to each other, and the compiler is free to treat them interchangeably. Thus, all bets are off when it comes to trying to infer X and Y from the type Base<X, Y>. All you know is that you'll get some types consistent with their constraints.

It's possible that the compiler will be able to return Y from Base<X, Y> extends Base<X, infer T> ? T : never, but there's no guarantee. And unfortunately, with your intermediate ExtendedObject class, you end up with the constraint type instead of the specific type you were hoping for.


If you want more of a guarantee, you should make sure that your types have a structural dependence on the types you care about. Generic types should always use their type parameters in some structural way. For example:

abstract class Base<T extends object, U extends SecondaryObjectConstraint> {
    t!: T;
    u!: U
}

So a Base<T, U> has a t property of type T and a u property of type U. And now you get the desired behavior:

type IOEO = InferSecondaryObject<ExtendedObject>;
/* type IOEO = {
    myProp: number;
} */

Playground link to code

  • Related