Declare interface of class only with methods signature without names

Viewed 351

Suppose I have a class with many methods, but I know for sure that their signature matches.

Is it possible to describe the interface of this class without describing the specific methods of this class in it? Like here:

interface IController {
  (input: string): number // any method without reference to its name
}

class Controller implements IController {
  method1(input: string): number { ...do something }
  method2(input: string): number { ...do something }
  ...
}

Or is it impossible?

3 Answers

The option to have an index signature (as @fk82 outlines in his answer) has the undesired consequence of forcing you to add an index signature to the class. This means that your class will be indexable by an arbitrary string, which might not be what you want.

If your goal is just to force the implementer class to only have methods with the given signature, a better option is to use a mapped type:

type IController<K extends PropertyKey> = { 
    [P in K]: (input: string) => number;
}

class Controller implements IController<keyof Controller> {
    method1(input: string): number { return input.length; }
    method2(input: string): number { return input === '' ? 0 : 1; }
}

let a = new Controller();
a['aa'] // not allowwed under no implicit any 

This has the bonus advantage of allowing the class to have some methods that do not conform to the signature if needed, but in an explicit way:

class Controller implements IController<Exclude<keyof Controller, 'special'>> {
    method1(input: string): number { return input.length; }
    method2(input: string): number { return input === '' ? 0 : 1; }
    special() { }
}

You can use an index signature

interface IController {
  [name: string]: (input: string) => number;
}

A small caveat is that the TypeScript compiler will now require you to add the index signature to each class which implements IController. I.e. you need to define your Controller class as follows:

class Controller implements IController {
    [name: string]: (input: string) => number;
    method1(input: string): number { return input.length; }
    method2(input: string): number { return input === '' ? 0 : 1; }
}

Here's a TS playground with a full example. Note that the index signature will be tested in assertions such as

const A = {
    m(input: string): number { return input.length; },
} as IController;

const B = {
    m(input: string): string { return input; }
} as IController;

and the assignment of B will will raise a type error because of the string return value.

You can hack something in order to fit your sought out solution, as per @FK82's answer, but that'd defeat the purpose of the Interface construct, which is to bind an object to a bunch of compile-time known signatures. How would the compiler know what method in particular you are referring to when referencing the Interface?

However, from what I can tell, instead of trying to cram an Interface-based solution, why not declare a functional abstraction in your executing code instead? Just describe the function signature and swap to the proper method as you see suit, since in JS/TS functions are a First-class citizen.

type Strategy = (input: string) => number;

class Controller implements IController {
    method1(input: string): number { ...do something }
    method2(input: string): number { ...do something }
}

function useMethod(f: Strategy): any {
  ...
  const i = f('my string');
  ...
}

function main() {
    const c = new Controller ();
    const method = chooseOne === true ? c.method1 : c.method2;
    useMethod (method);
}

This way of doing things is not too disimilar to the Strategy Pattern in OOP, however the FP solution is leaner and boasts what's in my opinion one of the better features of Javascript/Typescript.

Related