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.
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!
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.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With