DEV Community

Cover image for Class patterns for static interfaces
Philippe Poulard
Philippe Poulard

Posted on • Updated on

Class patterns for static interfaces

When working with classes, there are two types to consider: the type of the static side and the type of the instance side.

The most common is of course the instance side, and things are working like expected :

interface HasName {
    name: string
}

class Person implements HasName {
    name!: string
    birthDate!: Date
}
Enter fullscreen mode Exit fullscreen mode

In the example above, all the fields are describing an instance of a Person, that is defined partially with the interface HasName.

Then, we might add some convenient static methods :

class Person implements HasName {
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<Person> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}

// try it :
const bob = await Person.load('Bob')
bob.birthDate.getFullYear();
Enter fullscreen mode Exit fullscreen mode

At this point of the design of such outstanding application, we guess that some other classes will follow the same pattern for loading and saving data, therefore we create an interface for that purpose :

interface Storable<Type, Key> {
    load(key: Key): Promise<Type>
    save(item: Type): Promise<void>
}
Enter fullscreen mode Exit fullscreen mode

Since the static keyword can't be used on interfaces, I just removed it, but how to enforce my Person class to implement those methods as static ?

Language evolution proposal

There has been some discussions on that topic, but my preferred solution would be :

// unfortunately, this is not allowed by Typescript :
class Person implements HasName, static Storable<Person, string> {
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<Person> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}
Enter fullscreen mode Exit fullscreen mode

Unlike other proposals, it has the advantage to not restrict the interface to classes and keep the intend of Typescript of being a structural type system.

But that syntax doesn't exist : we must use the possibilities of the language.

Understanding class type duality

Let's recall that 2 types exist around each class :

  • the type of the static side,
  • the type of the instance side.

Before showing acceptable patterns, we must understand what is the type of the static side of a class. For that purpose, let's examine how they are managed on predefined classes, say let's have a look at String :

interface String {
    charAt(pos: number): string;
    charCodeAt(index: number): number;
    // etc, all instance methods
}

interface StringConstructor {
    new(value?: any): String;
    (value?: any): string;
    readonly prototype: String;
    fromCharCode(...codes: number[]): string;
}

declare var String: StringConstructor;
Enter fullscreen mode Exit fullscreen mode

So, String instances are fully defined by the interface definition, and at the end, the same String stuff has a type, the StringConstructor interface.

Strictly speaking, the name endorsed is not the best choice, because the string constructor is just the function new(value?: any): String; : all other definitions of that interface are static definitions, therefore the name StringStatic would have certainly been a better choice ; but let's refrain from rewriting history...

In fact, the last line declare var String: StringConstructor is used to enforce the built-in variable String to be of the type StringConstructor. But String as a variable doesn't refer to an instance but to the class.

Therefore, we might apply the same recipe for our own classes : what is needed is just a typed variable to which one can assign the class, it will enforce the class to honour that type.

Pattern 1 : class expression

Let's try it on our type Person ; first we can make some observations if we assign that class to some variable (actually, a constant !) :

Image description

In our preferred IDE, we see that person has the type typeof Person : this is the static side of the class Person. Unfortunately, unlike with the built-in String, we can't call the constant Person because that symbol already exist in the value space for the class itself, therefore we call it person.

Hopefully, we can use class expressions:

interface PersonInterface extends HasName {
    birthDate: Date
}
interface PersonStatic extends Storable<PersonInterface, string> {
    new(): PersonInterface
}
const Person: PersonStatic = class Person implements HasName {
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<PersonInterface> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}
Enter fullscreen mode Exit fullscreen mode

Ouch ! There are too much disadvantages with this pattern :

  1. we can't apply a decorator on the class
  2. generics are left out (if we had Person<Foo> ?)
  3. lots of boilerplate code are necessary

Let's take a closer look at the last issue. Why do we have this new interface PersonInterface ? This is because we lost the type typeof Person of the class : if we use Storable<typeof Person>, it would refer the type of the constant Person which is not what we want. On the other hand, Storable<Person> is not usable since Person is a constant. Do you follow ? Well... we need that new interface PersonInterface.

In fact, we have to fix the things lost when using the class as an expression.

Pattern 2 : static field of itself

One better solution, is to have a standalone typed variable that refers the class. But wait, where is the preferred place to define such a variable ? In the class itself as a static variable of course ! But wait, could we define an interface for such classes ? Sure we can :

interface HasStatic<Type> {
    class: HasStatic<Type>
}
Enter fullscreen mode Exit fullscreen mode

Then, we can define a type for the static side of our class :

type PersonStatic = Storable<Person, string> & HasStatic<Person>;

class Person implements HasName {
    static class: PersonStatic = Person; // 👈 
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<Person> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}
Enter fullscreen mode Exit fullscreen mode

The thing that enforce our static methods is due to the line static class: PersonStatic = Person : it has the type PersonStatic and has the value Person, which is the class itself.

I personally like that solution because it is in the spirit of the language evolution that I suggest before.

Pattern 3 : static block with field of itself

The following pattern is a variant of the previous one :

class Person implements HasName {
    static {
        const clazz: Storable<Person, string> = Person;
    }
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<Person> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}
Enter fullscreen mode Exit fullscreen mode

It has the advantage of being more straightforward and to drop the clazz constant after being set. Notice that class couldn't be used as the constant name; hence clazz.

Pattern 4 : class decorator

Another solution is to call a function that takes as argument the static definition of the class. The best way to apply that function on our class is by using the semantic of the decorator.

Here is the decorator factory, that does nothing and returns nothing, which means that the decorated class is not altered ; its role is to enforce its argument (the static side of our class) to be of the type given :

function HasStatic<Type>() {
    return (target: Type) => {};
}
Enter fullscreen mode Exit fullscreen mode

And this is how it has to be apply to our class :

@HasStatic<Storable<Person, string>>()
class Person implements HasName {
    name!: string
    birthDate!: Date
    static async load(name: string): Promise<Person> {
        // get data and create a new Person someway
    }
    static async save(person: Person) {
        // save the Person someway
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that we used a decorator factory instead of a decorator, because we wouldn't be able to use the latter like this : @HasStatic<Storable<Person, string>>.


Thank you for reading, Typescript Padawan !

Top comments (0)