skip to Main Content

I have an object with some properties and methods. How do I give it a method that accepts only the name of another one of its methods?

For example, consider the command method in this User class. We want command to accept the name of another method. Using keyof this narrows the acceptable type to User property names; how would we narrow it further to just User method names, without hardcoding them?

class User {
    nameFirst: string;
    nameLast: string;

    command(commandName: keyof this) {
        this[commandName].call(this);
    }

    sink() {}

    swim() {}
}

const alice = new User();
alice.command(`swim`); // Accepts `nameFirst` | `nameLast` | `command` | `sink` | `swim`; we want it to accept only `sink` and `swim`

3

Answers


  1. To narrow the acceptable types of the command method to only User method names without hardcoding them, you can combine conditional and mapped types.

    type FunctionPropertyNames<T> = {
      [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
    }[keyof T];
    
    class User {
      nameFirst: string;
      nameLast: string;
    
      command(commandName: FunctionPropertyNames<User>) {
        this[commandName].call(this);
      }
    
      sink() {
        console.log('Sink');
      }
    
      swim() {
        console.log('Swim');
      }
    }
    
    const alice = new User();
    alice.command('swim'); // This is now restricted to 'sink' and 'swim'
    
    Login or Signup to reply.
  2. TypeScript doesn’t have a built-in KeysMatching<T, V> type operator to extract the keys of a type T whose values match a certain other type V, as requested in microsoft/TypeScript#48992, but you can implement one yourself that works for some use cases, for example:

    type KeysMatching<T, V> =
      { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
    

    Then conceptually you’d make commandName of type KeysMatching<User, ()=>void> so that it only allows zero-arg methods. But this will cause a circularity warning (since the type of commandName depends on the type of User which depends on the type of command which depends on the type of commandName). You can circumvent this by omitting "command" from the keys to allow, using the Omit utility type:

    class User {
      nameFirst: string = "";
      nameLast: string = "";
    
      command(commandName: KeysMatching<Omit<User, "command">, () => void>) {
        this[commandName].call(this);
      }
    
      sink() { }
    
      swim() { }
    
      needsParam(x: string) { console.log(x.toUpperCase()) }
    }
    
    const alice = new User();
    alice.command("swim")
    alice.command("sink");
    alice.command("nameFirst"); // error, not accepted
    alice.command("needsParam"); // error, not accepted
    

    Playground link to code

    Login or Signup to reply.
  3. Not sure what you’re trying to do specifically but could this work for your use case?

    class User {
        nameFirst: string;
        nameLast: string;
    
        command(commandName: keyof this) {
          if (typeof this[commandName] === 'function') {
                this[commandName].call(this);
          } 
          else {
            return
          }
        }
    
        sink() {}
    
        swim() {}
    }
    
    const alice = new User();
    alice.command(`swim`);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search