Búsqueda de sitios web

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}`);
};