2
votes

In my app I have several classes which are used to create XML strings. Each class has a few methods that take some arguments and return a string. I want to specify this limitation so that methods with a different signature or other class members can't be added. To this end, here's where I got so far:

interface IRequestParser {
  [funcName: string]: (...args: any[]) => string;
}

and an example of a class:

class myParser implements IRequestParser {
  [funcName: string]: (...args: any[]) => string;

  func1(a) {
    return '';
  }
}

This prevents me from adding functions that don't return strings, or any other non-method member, so these aren't allowed inside myParser:

  a: string; // not allowed
  b() { // not allowed
    return 5;
  }

However, this has the effect of letting me call any function name from an instance of myParser without an alert:

const a = new myParser();
console.log(a.func1('a'));
console.log(a.func2(4, 5, ['s', 'b']));

The call to func1 makes sense and I would also get an error if I didn't supply a value for its argument a. However, I can also call non-existing function func2 and any other name.

Is there a way to both limit the type of class members while also avoiding this situation of calling any function with any name?

2

2 Answers

3
votes

You could also do this:

class MyParser implements Record<keyof MyParser, (...args: any[]) => string> {

  // okay
  func1(a: any) {
    return '';
  }

  // error: string is not assignable to (...args: any[]) => string
  a: string; 

  // error: number is not assignable to string
  b() { 
    return 5;
  }
}

const myParser = new MyParser();
myParser.func1("okay"); // works
myParser.funcNope("huh"); // error, no property funcNope

It's permissible to implement a concrete mapped type (like Record<>), and it is sometimes possible to have circular references (referring to keys via keyof is usually acceptable).

Hope that helps; good luck!

3
votes

The interface you use allows the class to be indexed by any string. We can't express the constraint you want with an interface (at least no way I can think of, maybe someone else has an idea).

You could use a function to constrain the class to only have functions with the signature while preserving the actual properties the class has:

type ParserFn = (...args: any[]) => string;
function createRequestParserClass<
  T extends new (...a: any[]) => Record<keyof InstanceType<T>, ParserFn>>(ctor: T) {
  return ctor
}
const myParser = createRequestParserClass(class {
  // a: string; // not allowed
  // b() { // not allowed
  //   return 5;
  // }

  func1(a: string) {
    return '';
  }
});

type myParser = InstanceType

const a = new myParser();
console.log(a.func1('a'));
console.log(a.func2(4, 5, ['s', 'b'])); // error now 
console.log(a.func1(1)); // also error