Home > Blockchain >  Why am i unable to infer type parameters from a class that implements an interface?
Why am i unable to infer type parameters from a class that implements an interface?

Time:11-09

Background

I have an interface that declares a single method whose return type depends on which type parameters are supplied to the interface.

interface Command<T, U = void> {
    execute(input: T): [U] extends [void] ? T | Observable<T>: T | U | Observable<T|U>;
}

I then have a utility type which should return the type parameters T and U for the supplied type.

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, A] :
    never;

Problem

Playground

Although GetCommandParamTypes works as expected when supplied with the Command interface, i'm struggling to debug why i'm not getting the result i expected when supplying it with a class that implements that interface.

 type type1 = GetCommandParamTypes<Command<string>>;
// [string, void]

type type2 = GetCommandParamTypes<Command<string, string>>;
// [string, string]

type type3 = GetCommandParamTypes<Command<string | number, string>>;
// [string | number, string]

type type4 = GetCommandParamTypes<Command<string | number, Function>>;
// [string | number, Function]

type type5 = GetCommandParamTypes<Command<string, number | Function>>;
// [string, number | Function]

type type6 = GetCommandParamTypes<TypeACommand>; // expected [string, void]
// [string, string]

type type7 = GetCommandParamTypes<TypeBCommand>; // expected [string, number | Function]
// [string | number | Function, string | number | Function]

type type8 = GetCommandParamTypes<TypeCCommand>; // expected [string, void]
// [string, string | Observable<string>]

type type9 = GetCommandParamTypes<MixedTypeCommand>; // expected [string | number, boolean | Function]
// [string | number | boolean | Function, string | number | boolean | Function]

type type10 = GetCommandParamTypes<MixedTypeObservableCommand>; // expected [string | number, boolean | Function]
// [string | number | boolean | Function, string | number | boolean | Function | Observable<string | number | boolean | Function>]

Question

Should it be possible to infer type parameters from a Class that implements an interface or is there something wrong with my implementation?

Could someone please explain what happens when a Class is inspected to infer type parameters or point me in the direction of any resources that might help explain it?

Update 1

Playground

If i remove the conditional type from the command return type (which is not the desired behaviour as the second type parameter is optional and i don't want void in the return type if it's not supplied) it does seem to get closer to what i was expecting. Or at least something a bit more workable.

interface Command<T, U = void> {
    execute(input: T): T | U | Observable<T | U>;
}

At first glance it appears that the execute method input type is being returned for infer IO and the return type is being returned for infer A.

But for TypeACommand that has the following signature:

execute(input: string): string | Observable<string>

[string, string] is returned, not [string, string | Observable<string>]

Where as TypeCCommand which has the following signature:

execute(input: string): Observable<string>

[string, string | Observable<string>] is actually returned instead of [string, Observable<string>]

And MixedTypeObservableCommand which also only has an Observable as the return type also returns all the type parameters of the Observable<...> and the Observable itself.

So:

execute(input: string | number): Observable<string | number | Function | boolean>

becomes:

[string | number, string | number | boolean | Function | Observable<string | number | boolean | Function>]

Update 2

This is a link to a previous working example i created, before also adding Observable to the return type of the Command interface.

Playground

There at least appeared to be consistency in the types returned for infer IO and infer A for this version, where IO was the execute methods input type and A was the execute methods return type.

I was then able to use Exclude to calculate the corresponding types in relation to the Command interface

CodePudding user response:

The reason why this happens if because when checking is T in GetCommandParamTypes is an instance of Command it doesn't actually check if the class you provided implements the interface. Typescript has structural, not nominal typings, so what it does is compare the signatures of Command<..., ...> and of T. Type TypeACommand has a signature

type TypeOfACommandObject = {
    execute(input: string): string | Observable<string>
}

There is nothing suggesting it implements Command, it is just an object that has a method execute with particular arguments and return types. If you now pass this object to GetCommandParamTypes you will find the exact same problem: you would probably expect the return type to be [string, void], but it's actually [string, string]. And there is no contradiction, because Command<string, string> has signature

{
    execute(input: string): string | string | Observable<string | string>
}

Which is indistinguishable from the signature of Command<string, void>. So you cannot type it that easily.

The reason it will work correctly when you try GetCommandParamType<Command<string, void>>, even though the signature of Command<string, void> is as described above and you would probably expect it to be inferred incorrectly as well, is because of typescript's compiler implementation. I assume that when checking if type T is an instance of Command it first checks if T is Command. In this case it is literally and instance of Command, so it just skips comparing the structure and yields the correct type arguments. TypeOfACommandObject and TypeACommand however are not direct instances of Command, even though the latter implements it, so this shortcut fails. This is a speculation though, I'm not familiar with typescript implementation.


Speaking about fixing it, I can't really think of a good way without restructuring the return type of execute so that T and U are not merged. I could think of branded types like adding a special unique signature to return type of command using unique symbol type, but it will be very hard to use both when implementing Command and when using it outside. You should probably try to separate T and U somehow


UPD:

In the updated example it works because of the different definition of GetCommandTypeParams

type ReplaceNever<T, U> = [T] extends [never] ? U : T;

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, ReplaceNever<Exclude<A, IO>, void>] :
    never;

If you stick this same definition in the first example, you will find that it also almost works as expected, the only problem is the last class with Observables. So you'll need a small adjustment

type GetCommandParamTypes<T extends Command<unknown, unknown>> = 
    T extends Command<infer IO, infer A> ?
    [IO, ReplaceNever<A extends Observable<infer U> ? Exclude<U, IO> : Exclude<A, IO>, void>] :
    never;

Sandbox

This solution has a couple of weirdnesses like you won't be quite able to extract types from classes that implement Command<string | number, string | Function> for example, but if you can live with it then it's fine

  • Related