Search code examples
javascripttypescriptclass

Restrict typescript class methods implementing interface


Currently if I create a class that implements an interface, the class created will all methods not included in the interface. Here's an example:

interface ExampleTypes {
  alpha();
}

class Example implements ExampleTypes {
  alpha () {
    return true
  }
  beta () {
    return true
  }
}

I am looking for a way to restrict the methods a given class can have.

This is something I also tried:

class ExampleSource {
  alpha () {
    return true
  }
}

class Example implements Partial<ExampleSource> {
  alpha () {
    return true
  }
  beta () {
    return true
  }
}

And this:

class ExampleSource {
  alpha () {
    return true
  }
}

class Example implements ExampleSource {
  alpha () {
    return true
  }
  beta () {
    return true
  }
}

Which is unintuitive. I'd like beta to not be allowed in Example.

This is the functionality that works but using a function and not a class:

interface ExampleType {
  alpha?();
  beta?();
}

This is value:

function Example(): ExampleType {
  return {
    alpha: () => true,
  };
}

This throws a typescript error:

function Example(): ExampleType {
  return {
    alpha: () => true,
    meow: () => true,
  };
}

Ideally I can have this same functionality but with classes.


Solution

  • It's an odd request, since having extra methods won't stop you from using the class as if they weren't there. TypeScript doesn't really have a lot of support for excluding extra properties or methods from types; that is, there's currently no direct support for exact types as requested in microsoft/TypeScript#12936.

    Luckily you can sort of get this behavior by making a self-referential conditional, mapped type:

    type Exactly<T, U> = { [K in keyof U]: K extends keyof T ? T[K] : never };
    

    If you declare that a type U is Exactly<T, U>, it will make sure U matches T, and that any extra properties are of type never. Self-referential/recursive/circular types don't always compile, but in this case you're only referring to keyof U inside the definition of U, which is allowed.

    Let's try it:

    interface ExampleTypes {
      alpha(): boolean; // adding return type
    }
    
    // notice the self-reference here
    class Example implements Exactly<ExampleTypes, Example> {
      // okay
      alpha() {
        return true;
      }
    
      // error!  Type '() => boolean' is not assignable to type 'never'.
      beta() {
        return true;
      }
    }
    

    Looks like it works!