Search code examples
typescript

Make TypeScript infer generic argument


I have a base and a descendant type

interface IInferredTypeBase {
}
interface IInferredType extends IInferredTypeBase {
    myProperty;
}

In reality, there are many descendants in that hierarchy.

Now, I also have a type which is generic for this hierarchy:

interface IGenericForInferredType<T extends IInferredTypeBase> {
}

What I want is to make TypeScript automatically infter the generic argument in a situation like this:

type ICallback<TInferredType extends IInferredTypeBase, TGenericForInferredType extends IGenericForInferredType<TInferredType>> = (instance: TInferredType) => any;

function register<
    TInferredType extends IInferredTypeBase,
    TGenericForInferredType extends IGenericForInferredType<TInferredType>
>(
    param1: new () => TGenericForInferredType,
    callback: ICallback<TInferredType, TGenericForInferredType>
) {
    //...
}

So, there is the register function taking 2 arguments: param1 is any kind of a descriptor of a type which is IGenericForInferredType<TInferredType>, just for the other parameter callback to have its argument as TInferredType inferred. But it does not work.

So, let's take a concrete example on this.
There is this class for param1:

class ClassImplementingGenericForInferredType implements IGenericForInferredType<IInferredType> {
}

And here is an exmaple call:

register(ClassImplementingGenericForInferredType, (instance) => {})

The type of instance is however the base type IInferredTypeBase and not IInferredType.

How could I make it infer IInferredType?

Meaning I don't want to write the word IInferredType anywhere when I call register, I want TypeScript to figure it out for me.

Motivation: ClassImplementingGenericForInferredType, IInferredType, IInferredTypeBase --> These are generated from C#, their hierachical and generic relations are defined in C#, I don't want to write it 2x

Here is a TypeScript Playground link demonstrating the issue, and as a fallback, a github repo


Solution

  • TypeScript's type system is structural, not nominal. So a generic type like

    interface Gen<T extends Base> {}
    

    where the generic type parameter T does not appear in the definition (it's just {}) has no structural dependency on T. And therefore T cannot possibly be inferred from it. In the above, Gen<A> and Gen<B> are both just {}, and looking at {} cannot reliably give you any information about A and B. See the relevant TS FAQ entry.

    You need some structural dependence, like

    interface Gen<T extends Base> {
        t: T;
    }
    

    Again

    type ICallback<T extends Base, G extends Gen<T>> = (instance: T) => any;
    

    has no structural dependence on G, and therefore you cannot possibly infer G from it. For this type, G presumably serves no purpose whatsoever. We can remove G from that definition, and indeed, remove ICallback entirely because the indirection doesn't buy us anything; we can just use (t: T) => any.


    From there, you need to make sure that you have as few generic type parameters as necessary. The minimum code necessary to make your example work looks like

    function register<T extends Base>(
      param1: new () => Gen<T>, 
      callback: (t: T) => any
    ) { }    
    
    interface Ext extends Base {
        myProperty: string;
    }
    class ClassImplementingGenericForInferredType implements Gen<Ext> {
        declare t: Ext
    }
    register(ClassImplementingGenericForInferredType, (instance) => {
        instance.myProperty = "...";
    })
    

    Here calling register() with ClassImplementingGenericForInferredType allows TypeScript to infer that T is Ext. Note that the implements clause on the class declaration does not cause that inference to succeed. Such clauses have no effect whatsoever on inference, and are only used for type checking the body of the class after the fact. (You can remove the implements clause and see that it doesn't change things.) It's the t property of type Ext that lets TypeScript infer T. (If you try to remove the t property you'll see how everything blows up.) Again, structural typing is fundamental to TypeScript; declaration sites don't count as much.

    And once T is inferred as Ext, then the contextual type of the callback parameter instance is Ext, and hence instance.myProperty exists.

    Playground link to code