Search code examples
typescriptencapsulation

Typescript: abstract class and module-level visibility


I saw that in Typescript you can emulate module visibility with interfaces, but I don't know if it is possible to achieve it in the following scenario:

abstract class ConnectionTarget
{
    // callback that subclasses must implement
    protected abstract onConnection: (conn: Connection) => void;

    // property that must be available to subclasses
    protected get connections(): Readonly<Iterable<Connection>>
    {
        return this.conns;
    }

    // private field needed for previous property
    private conns: Connection[] = [];

    // method that SHOULD HAVE MODULE VISIBILITY
    // my module should be able to add connections,
    // but my users shouldn't
    private addConnection(conn: Connection)
    {
        this.conns.push(conn);
        this.onConnection(conn);
    }
}

// my function that needs access to the private members
// the parameter is a user-provided subclass of ConnectionTarget
function doMagicThings(target: ConnectionTarget, source: ConnectionSource)
{
    // do magic tricks here ...

    // method that should be module-protected, like addConnection
    let aConnection: source.createConnection();

    target.addConnection(aConnection);
}

I'd like my users to extend ConnectionTarget, having to implement onConnection and being able to only use the property connections, with everything else hidden to them.

EDIT: example usage

// class in user code
class MyConnectionTarget extends ConnectionTarget
{
    // users must implement this abstract method
    onConnection(conn: Connection)
    {
        // user specific code here
        // ...

        // can use property 'connections'
        console.log(this.connections)

        // should error here:
        // should not allow to use the following method
        this.addConnection(new Connection());
    }
}

Solution

  • You can do that by exporting an interface which declares the public methods without exporting the class itself.
    You will then need a factory function which is exported by the module to be able to instantiate the class, something like:

    export interface IConnectionTarget {
        // public methods will be declared here, i.e:
        myMethod(): void;
    }
    
    abstract class ConnectionTarget implements IConnectionTarget {
        private conns: Connection[] = [];
    
        protected abstract onConnection: (conn: Connection) => void;
    
        protected get connections(): Readonly<Iterable<Connection>> {
            return this.conns;
        }
    
        public addConnection(conn: Connection) {
            this.conns.push(conn);
            this.onConnection(conn);
        }
    
        public myMethod() {}
    }
    
    export function createConnectionTarget(): IConnectionTarget {
        // create an instance here and return it
    }
    

    (code in playground)


    Edit

    Without understanding what you're trying to do better, at seems that you have a few options, but none of them is very pretty:

    (1) Keep the method private and when trying to access it cast to any:

    let aConnection: source.createConnection();
    (target as any).addConnection(aConnection);
    

    (2) Save the setter in the ctor to a module level store:

    type Adder = (conn: Connection) => void;
    const CONNECTION_ADDERS = new Map<ConnectionTarget, Adder>();
    
    abstract class ConnectionTarget {
        protected constructor() {
            CONNECTION_ADDERS.set(this, this.addConnection.bind(this));
        }
    
        private addConnection(conn: Connection) { ... }
    }
    

    And then to use it:

    let aConnection: source.createConnection();
    CONNECTION_ADDERS.get(aConnection)(aConnection);