Restrict typescript class methods implementing interface
Asked Answered
H

2

8

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.

Hufuf answered 7/12, 2018 at 18:21 Comment(10)
Why? That's just not how OOP Programming works, especially considering the SOLID principles.Sansen
I suppose that is something that you could do in the constructor function. Maybe have a model that a given instance of the class should conform to, and have the constructor strip everything else off that doesn't fit the model. This could be done dynamically by passing in different models that represent a subset of the functions/properties of your classFlown
there is no thing like final in TSIlldefined
@Illdefined Not sure final is what the OP is looking for. They want TypeScript to enforce a strict definition of a class by using another interface. No additional properties should exist on the class that do not exist in the interface.Flown
@Flown Yes I would like to have many classes that implement a true Partial from a common source (interface or another class) where the classes cannot have any extra methods, only methods defined from the source.Hufuf
Added some more updates on things i've tried.Hufuf
TS does not have exact types, so this is generally not going to be possible. Extra stuff is allowed to satisfy a type. Why do you want to do this?Ikkela
@ThomasReggi: Is it actually important that the classes don't have extra members? Or do you want to restrict uses of those classes? Because you can do the latter by just upcasting to Partial<ExampleSource>. But to actually try to restrict what methods a class contains is really suspect.Illegality
@MarkPeters Yes I would like the classes not not have extra members. The uses of those classes is not relevant to this question.Hufuf
So inferring from that, that implies that it's the runtime members of the prototype that you care about, not satisfying the type checker. If that's the case are you sure types/interfaces are the right path here? After all they're mostly only relevant in a static context, at compile time. Vs an approach like @Flown suggests, where you inspect the object at runtime.Illegality
A
27

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!

Abirritate answered 7/12, 2018 at 18:50 Comment(0)
B
0

I found the answer by @jcalz to be extremely useful, however it breaks the behavior of "implements" because it only checks in one direction. For example, the following code does not report any errors, even though the Example class does not implement the interface.

Here's a Playground

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

interface IExample {
   foo: string;
}

// This should have an error but does not
class Example implements Exactly<IExample, Example> {}

The solution is to check both the class and interface to make sure all properties are present and that there are no additional properties:

Here's a Playground

type Exactly<Interface, Klass> = { 
   [K in keyof Interface]: K extends keyof Klass ? Interface[K]: never;
} & {
   [K in keyof Klass] : K extends keyof Interface ? Interface[K] : never;
};

interface IExample {
   foo: string;
}

// Error - "foo" not implemented
class Example implements Exactly<IExample, Example> {}

class Example2 implements Exactly<IExample, Example2> {
   foo!: string;
   bar!: number; // Error - "bar" not assignable to "never"
}
Bisutun answered 17/7, 2024 at 0:15 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.