Reputation: 43
I'm trying to define a method signature where a parameter should conform to an interface.
interface Command {}
class HelloCommand implements Command {
constructor(public readonly name: string) {}
}
type HandleCommandFn = (command: Command) => Promise<any>;
const handleHello: HandleCommandFn = (
command: HelloCommand
): Promise<string> => {
return Promise.resolve(`Hello ${command.name}`);
};
I get the following error from the compiler:
src/index.ts:9:7 - error TS2322: Type '(command: HelloCommand) => Promise<string>' is not assignable to type 'HandleCommandFn'.
Types of parameters 'command' and 'command' are incompatible.
Property 'name' is missing in type 'Command' but required in type 'HelloCommand'.
9 const handleHello: HandleCommandFn = (
I also tried the following variations :
type HandleCommandFn = <C extends Command>(command: C) => Promise<any>;
type HandleCommandFn = <C extends Command = Command>(command: C) => Promise<any>;
UPDATE
With the help of answers below, I managed to get the following piece of code (my goal was to define a type-safe method decorator) :
interface Command {
getDescription(): string;
}
type HandleCommandFn<C extends Command = Command> = (
command: C
) => Promise<any>;
function HandleCommand<C extends Command = Command>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<HandleCommandFn<C>>
): void {
const method = descriptor.value;
if (method) {
// Do something
}
}
class HelloCommand implements Command {
constructor(public readonly name: string) {}
public getDescription(): string {
return "Say hello";
}
}
class HelloCommandHandler {
@HandleCommand // OK
public execute(command: HelloCommand): Promise<string> {
return Promise.resolve(`Hello ${command.name}`);
}
@HandleCommand // Error (wrong parameter type)
public execute2(command: string): Promise<string> {
return Promise.resolve(`Hello ${command}`);
}
@HandleCommand // Error (wrong return type)
public execute3(command: HelloCommand): string {
return `Hello ${command.name}`;
}
}
Upvotes: 2
Views: 84
Reputation: 33101
This is because function arguments are contravariant. Consider this example:
interface Command { }
interface HelloCommand extends Command {
name: string
}
declare var command: Command
declare var helloCommand: HelloCommand
command = helloCommand // ok , is assignable
It is intuitively, helloCommand
is assignable to command
because it is more specific. And it is easy to understand why subtype is assignable to supertype.
But if you look at this example, things are not obvious anymore:
let commandFn = (command: Command) => void 0
let helloCommandFn = (command: HelloCommand) => void 0
commandFn=helloCommandFn // error, is not assignable anymore
FUnction with HelloCommand
argument (subtype of Command) is not assignable anymore to function with Command
(supertype).
This is how contravariance works. Direction of inheritance has changed is opposite direction.
What you are trying to do is unsafe, or in other words - unsound
behavior.
In order to trick compiler you can add extra generic:
type HandleCommandFn<T extends Command> = <U extends HelloCommand>(command: T & U) => Promise<any>;
// ok
const handleHello: HandleCommandFn<Command> = (
command
): Promise<string> => {
return Promise.resolve(`Hello ${command.name}`);
};
Upvotes: 1
Reputation: 271
This should work
interface Command {}
class HelloCommand implements Command {
constructor(public readonly name: string) {}
}
type HandleCommandFn<C extends Command = Command> = (
command: C
) => Promise<any>;
const handleHello: HandleCommandFn<HelloCommand> = (
command
): Promise<string> => {
return Promise.resolve(`Hello ${command.name}`);
};
Upvotes: 2