Vaccano Vaccano - 3 months ago 10
TypeScript Question

Are interfaces useful in Typescript (architecturally speaking)?

I am coming to Typescript from a C# background.

In C#, if I have a class that is called

BillingService
and it has methods that calls a billing service (can make payments, give account balances etc), then for the parts of my code that need to call that class I would setup a separate assembly that has a
IBillingService
defined.

The rest of my code would use dependency injection to get an
IBillingService
and would never have a reference to the assembly that actually implements
BillingService
. This facilitated unit testing, loose coupling and a few other benefits around data hiding.

When I first started using TypeScript I kept doing that. But now I am wondering if there is any real benefit to it. Judicious use of public and private seems sufficient.

There are no separate assemblies in Typescript. Dependency Injection can inject the actual class in as if it had implemented an interface (at least Aurelia's Dependency Injection can). And JavaScript Unit Testing frameworks seem to get confused by Interfaces anyway.

So, here is my question:

For the scenario I have outlined above, is there a use case for interfaces in TypeScript? If so, what is it?

Answer

Like in some other OO languages, in typescript it's also not possible to have multiple inheritance but it's possible to implement more than one interface, that's one use case for which interfaces are good for.

Another reason is if you want to spread different implementations across different namespaces/modules but you want them all to implement a specific set of methods:

namespace callbacks {
    export interface Callback<T> {
        getName(): string;
        execute(): T;
    }
}

namespace mynamespace1 {
    export class Callback implements callbacks.Callback<string> {
        public getName(): string {
            return "mynamespace1.Callback";
        }

        public execute(): string {
            return "executed";
        }
    }
}

namespace mynamespace2 {
    export class Callback implements callbacks.Callback<boolean> {
        public getName(): string {
            return "mynamespace2.Callback";
        }

        public execute(): boolean {
            return true;
        }
    }
}

But the best reason (in my opinion) is that it lets you hide the implementing classes inside a closure so that no one can create them directly but only via factory functions or some actions:

namespace logging {
    const httpLoggingEndpoint: URL = new URL(...);
    const fileLoggingFilePath: string = "LOG_FILE_PATH";

    export enum LoggerType {
        Console,
        Http,
        File
    }

    export interface Logger {
        log(message: string): void;
    }

    export function getLogger(type: LoggerType): Logger {
        switch (type) {
            case LoggerType.Console:
                return new ConsoleLogger();

            case LoggerType.Http:
                return new HttpLogger();

            case LoggerType.File:
                return new FileLogger();
        }
    }

    class ConsoleLogger implements Logger {
        public log(message: string): void {
            console.log(message);
        }
    }

    class HttpLogger implements Logger {
        public log(message: string): void {
            // make a request to httpLogingEndpoint
        }
    }

    class FileLogger implements Logger {
        public log(message: string): void {
            // log message to the file in fileLoggingFilePath
        }
    }
}

This way no one can directly instantiate a logger because none of the actual classes are exported.

Another point to make on this subject is that in typescript classes can be treated as interfaces:

class Logger {
    public log(message: string) {
        console.log(message);
    }
}

class HttpLogger implements Logger {
    public log(message: string) {
        // log using an http request
    }
}

Which is used for mixins for example, so in practice my first two scenarios can be done with classes as well, though my last example won't work as well with classes because then you can just instantiate the base class and by doing that bypass the "safety mechanism" of not being able to directly calling the different constructors.

Comments