Sylvain Desvé
Sylvain Desvé

Reputation: 43

How to type a method with an interface as parameter

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

Answers (2)

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

Sri
Sri

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

Related Questions