typescript - Cómo tipar un método con una interfaz como parámetro
Estoy intentando definir la firma de un método donde un parámetro debe cumplir con una interfaz.
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}`);
};
Recibo el siguiente error del compilador:
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 = (
También probé las siguientes variaciones:
type HandleCommandFn = <C extends Command>(command: C) => Promise<any>;
type HandleCommandFn = <C extends Command = Command>(command: C) => Promise<any>;
ACTUALIZACIÓN
Con la ayuda de las respuestas a continuación, logré obtener el siguiente fragmento de código (mi objetivo era definir un decorador de métodos seguro en tipos) :
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}`;
}
}
Respuesta #1
Esto debería funcionar
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}`);
};
Respuesta #2
Esto se debe a que los argumentos de una función son contravariantes. Considera este ejemplo:
interface Command { }
interface HelloCommand extends Command {
name: string
}
declare var command: Command
declare var helloCommand: HelloCommand
command = helloCommand // ok , is assignable
Intuitivamente, helloCommand se puede asignar a command porque es más específico. Y es fácil entender por qué un subtipo se puede asignar a un supertipo.
Pero si miras este ejemplo, las cosas ya no son obvias:
let commandFn = (command: Command) => void 0
let helloCommandFn = (command: HelloCommand) => void 0
commandFn=helloCommandFn // error, is not assignable anymore
La función con el argumento HelloCommand (subtipo de Command) ya no se puede asignar a una función con Command (supertipo).
Así es como funciona la contravarianza. La dirección de la herencia que ha cambiado es en dirección opuesta. Lo que estás tratando de hacer es inseguro, o en otras palabras, un comportamiento no seguro.
Para engañar al compilador puedes agregar un genérico extra:
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}`);
};
