8

I'm trying to infer the return type of a method from the generic of an argument passed in. However, the argument is an implementation from a generic interface, so I would assume typescript inferencing would have determined the type from the argument's base.

Example code:

interface ICommand<T> {}

class GetSomethingByIdCommand implements ICommand<string> {
  constructor(public readonly id: string) {}
}

class CommandBus implements ICommandBus {
  execute<T>(command: ICommand<T>): T {
    return null as any // ignore this, this will be called through an interface eitherway
  }
}

const bus = new CommandBus()
// badResult is {}
let badResult = bus.execute(new GetSomethingByIdCommand('1'))

// goodResult is string
let goodResult = bus.execute<string>(new GetSomethingByIdCommand('1'))

What I'd like to do is the first execute call and have typescript infer the correct return value, which is string in this case based on what GetSomethingByIdCommand was implemented from.

I've tried playing around with conditional types but not sure if this is a solution or how to apply it.

2
  • 1
    Perhaps the problem here is that there are no restrictions on your ICommand<T> interface in regards to how T is used. Because of that, your GetSomethingByIdCommand class also implicitly implements ICommand<number> (for example). Indeed it seems that the class is an implementation of ICommand<T> for any type T. Given that, how is TypeScript to choose which type to infer? Commented Mar 21, 2019 at 23:42
  • 1
    @CRice I don't quite understand this. How does GetSomethingByIdCommand implicitly implement ICommand<number> when it explicitly implements ICommand<string>? Can you elaborate? Commented Mar 22, 2019 at 0:13

3 Answers 3

10

Your problem is that ICommand<T> is not structurally dependent on T (as mentioned in @CRice's comment).

This is not recommended. (⬅ link to a TypeScript FAQ entry detailing a case that is almost exactly the same as this one, so that's as close to an official word as we're likely to get here)

TypeScript's type system is (mostly) structural, not nominal: two types are the same if and only if they are the same shape (e.g., have the same properties) and it has nothing to do with whether they have the same name. If ICommand<T> isn't structurally dependent on T, and none of its properties have anything to do with T, then ICommand<string> is the same type as ICommand<number>, which is the same type as ICommand<ICommand<boolean>>, which is the same type as ICommand<{}>. Yes, those are all different names, but the type system isn't nominal, so that doesn't count for much.

You can't rely on type inference to work in such cases. When you call execute() the compiler tries to infer a type for T in ICommand<T>, but there's nothing from which it can infer. So it ends up defaulting to the empty type {}.

The fix for this is to make ICommand<T> structurally dependent in some way on T, and to make sure any type that implements ICommand<Something> does so correctly. One way to do this given your example code is this:

interface ICommand<T> { 
  id: T;
}

So an ICommand<T> must have an id property of type T. Luckily the GetSomethingByIdCommand actually does have an id property of type string, as required by implements ICommand<string>, so that compiles fine.

And, importantly, the inference you want does indeed happen:

// goodResult is inferred as string even without manually specifying T
let goodResult = bus.execute(new GetSomethingByIdCommand('1'))

Okay, hope that helps; good luck!

Sign up to request clarification or add additional context in comments.

4 Comments

Great answer. This is certainly not very intuitive if you're coming from a different language that supports generics, like Java for instance.
Also, if you are in a situation where you cannot rely on having a value for your tag property (in the above, id), you can define it as a member and use the definite assignment operator to avoid having to set it: id!: string;
@y2bd could you explain this a bit more? I had issues using it in the interface, so I ended up using id?: string that also applied to the class implementation.
@ShawnMclean Here is an example: repl.it/repls/WoozyStupidAtoms
2

Typescript seems to be able to correctly infer the type if the concrete type is coerced into its generic equivalent before it is passed to ICommandBus.execute():

let command: ICommand<string> = new GetSomethingByIdCommand('1')
let badResult = bus.execute(command)

Or:

let badResult = bus.execute(new GetSomethingByIdCommand('1') as ICommand<string>)

This isn't exactly an elegant solution, but it works. Clearly typescript generics are not very feature complete.

Comments

1

TS cannot infer the interface that method is implementing in the way you want it to.

What is happening here is that when you instantiate a new class with:

new GetSomethingByIdCommand('1') 

The result of instantiating a new class is an object. For that reason execute<T> will return an object instead of the string that you expect.

You will need to do the type check after execute function returns a result.

In the case of object vs string, you can just do a typeof check.

const bus = new CommandBus()
const busResult = bus.execute(new GetSomethingByIdCommand('1'));
if(typeof busResult === 'string') { 
    ....
}

This works fine at runtime, when typescript is compiled to just plain JS.

In case of objects or arrays (which are also objects :D), you would use a type guard.

Type guard tries to cast an item to something and checks if a property exists and infers what model was used.

interface A {
  id: string;
  name: string;
}

interface B {
  id: string;
  value: number;
}

function isA(item: A | B): item is A {
  return (<A>item).name ? true : false;
}

2 Comments

What the question really asks is how to make the type inference work in an intuitive way. badResult is expected to have string type because the execute() is given an object which implements ICommant<T> and T is bound to string...
Unfortunately TS inferrence is a bit limited in that scenario. You cannot infer the implemented interface in that way. Result of instantiating a new class is always an object, so the return type is inferred as an object for that reason.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.