Typescript: exploring the concept of abstract classes and module-level visibility

While exploring Typescript, I came across a way to mimic module visibility using interfaces as discussed in this Github comment. However, I am unsure if it is feasible to achieve the same in a specific scenario:

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

    // property that must be accessible 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);
    }
}

// function requiring access to private members
// parameter is a user-provided subclass of ConnectionTarget
function doMagicThings(target: ConnectionTarget, source: ConnectionSource)
{
    // perform magic operations here ...

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

    target.addConnection(aConnection);
}

I want users to extend ConnectionTarget, mandating implementation of onConnection while only being able to utilize the connections property, with everything else concealed from 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());
    }
}

Answer №1

To achieve this, you can export an interface that declares the public methods without exporting the actual class itself.
Subsequently, a factory function must be exported by the module to enable instantiation of the class. Here is an example:

export interface IConnectionTarget {
    // Declaration of public methods goes here, for instance:
    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 {
    // Instantiate the class here and then return it
}

(Code available in the playground)


Edit

If you're uncertain about your approach or are looking for alternatives, consider these less elegant solutions:

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

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

(2) Store the setter in a module-level variable within the constructor:

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) { ... }
}

Usage would involve:

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilizing interpolation for a CSS class defined in an external file within Angular 2

Is it feasible to send a variable to a CSS class in an external CSS file within Angular 2, such as: import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', sty ...

The validator is incorrectly diagnosing the input as 'invalid' when in reality it is not

Currently, I am working on creating a text field that must not be empty and should also not start with a specific set of characters (let's say 'test'). For example, I want testxyz or an empty field to be considered invalid, while anything e ...

Using React's `cloneElement` to make modifications to a child component while maintaining the reference within a functional component

In the past, I had references in my component while rendering, and it was functioning as expected: // props.children is ReactElement<HTMLDivElement>[] const [childRefs] = useState<RefObject<any>[]>(props.children.map(() => createRef()) ...

Using the Google Identity Services JavaScript SDK in conjunction with Vue and TypeScript: A comprehensive guide

I am currently working on authorizing Google APIs using the new Google Identity Services JavaScript SDK in my Vue / Quasar / TypeScript application. Following the guidelines provided here, I have included the Google 3P Authorization JavaScript Library in ...

finding corresponding key in ngFor

I am curious to see if it is possible to match a key in *ngfor. Here is an example of my code on StackBlitz. However, when I try this method, it only displays No data. HTML <ul> <li *ngFor="let course of courses; index as i"> ...

What is the proper technique for utilizing private fields in TypeScript?

Every time I attempt to execute the code below that involves a private field, I encounter an "Invalid character" issue at the location of #. class MyClass { #x = 10; } Here is the content of my tsconfig.json file: { "compilerOptions": { ...

Working with objects in *ngFor in Ionic 2

I am utilizing Ionic 2 and attempting to display the content of a key-value array using an object. In order to display my collection, I am using a pipe in my HTML. Below is my HTML code: <ion-list> <ion-item *ngFor="let event of this.pdata. ...

Unusual manifestation of the Array

I defined a list array as: list: string[] = []; and added elements to it like this: let infoFile: string = fileReader.result; this.list.push(infoFile); After checking the console log, I noticed something strange [] 0 : "data:image/jpeg;base ...

Can you please explain the significance of classes <T> and <U> in Angular 2?

After diving into the Angular 2 TypeScript documentation, I have come across these two classes frequently. Can someone provide a detailed explanation of what they are? One example code snippet from the QueryList API documentation showcases: class QueryLi ...

Error: Unable to use import statement outside of a module when deploying Firebase with TypeScript

Every time I try to run firebase deploy, an error pops up while parsing my function triggers. It specifically points to this line of code: import * as functions from 'firebase-functions'; at the beginning of my file. This is a new problem for me ...

How to invoke a function from a different controller in Ionic 2

Is it possible to call a function in another controller within Ionic 2? Here is my code where I want to call the loadPeople function in the tab controller. home.ts import { Component } from '@angular/core'; import { NavController } from ' ...

What is the best way to restore an Angular 4 FormGroup to its initial state?

I have been delving into Angular4, specifically reactive forms. While experimenting with the Heroes demo, I encountered a hurdle. On this Plunker example, we are required to select a superhero and view their corresponding addresses. I added a reset button ...

Renovating Code: Swapping out a data type (and refreshing imports) - Tips and Tricks

For example, in our code base, we currently utilize a type OldType from the npm package @old-package. Our goal is to substitute it with our custom type NewType, which can be accessed through @new-package. Illustration Existing imports import { AnyImport ...

Overriding a library function with the same namespace name in a mocking scenario

In a scenario akin to a previous inquiry, my goal is to mock an external library utilizing sinon. The challenge lies in the fact that this library exports two functions and a namespace under the identical name of FastGlob. While I possess a basic understa ...

An array consisting of functions that specifically retrieve keys from an object

I have a user object with properties like id and name. interface User { id: string; name: string; } There is also an UpdateUserBody type that allows updating only the 'name' property of the user in the backend. type UpdateUserBody = Pick< ...

The call to Contentful's getAsset function resulted in an undefined value being

I am facing a challenge while trying to fetch an asset, specifically an image, from Contentful and display it in my Angular application. Despite seeing the images in the Network log, I keep encountering an issue where the console.log outputs undefined. Any ...

Tips for detecting the end of a scroll view in a list view

I've encountered an issue with my scrollView that allows for infinite scrolling until the banner or container disappears. What I'm aiming for is to restrict scrolling once you reach the last section, like number 3, to prevent the name part from m ...

Following the recent update to webpack-dev-server and webpack, certain modules are being requested that do not exist in the project

Recently, I made updates to my project that involved Vue.js and Typescript. After updating webpack and webpack-dev-server, I encountered a problem where certain modules were missing when attempting to run the project in development mode. Here is some addi ...

What is the reason behind typescript-eslint's insistence on using camelCase for enumMember?

The TypeScript documentation showcases enum examples with PascalCase for enum members, like: this enum Direction { Up = 1, Down, Left, Right, } However, @typescript-eslint/naming-convention mandates camelCase over PascalCase, resulting in: enum Di ...

How do you obtain the string name of an unknown object type?

In my backend controllers, I have a common provider that I use extensively. It's structured like this: @Injectable() export class CommonMasterdataProvider<T> { private readonly route:string = '/api/'; constructor(private http ...