Steps for Creating a Private Constructor in a Module while Allowing External Construction

I am looking for a way to restrict the construction of a class within a module to only be possible through a helper function from that same module. This would prevent any external users of the class from constructing it without using the designated helper function.

For example, the usage could look like this:

//module: email.ts

export class Email {
    //implementation details
}

export function tryParse(x: string): Email | null {
    //implementation details
}

Later on, I want to utilize it in this manner:

//module: email_usage.ts
import {Email, tryParse} from "./email.ts"

//works
let x?: Email = tryParse("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="c0aab5a8b580b9a1a8afafeea3afad">[email protected]</a>")

//but this should fail with an compiler error
let y: Email = new Email("foo")

I have come up with a solution for the above scenario, but I feel like it's quite unconventional.

export class Email {
    //I need to make the constructor protected here so it cant be accessed outside
    protected constructor(x: string) {
        this._value = x
    }
    private _value: string
    get Value () {
        return this._value
    }
}

//then I create a "Factory" class that extends from Mail so I have access to the protected constructor
class EmailFactory extends Email {
    // and then I create a "public" static method to finally create an Email instance
    static create(x: string): Email {
        return new Email(x)
    }
}

export function tryParse(x: string): Email | null {
    return EmailFactory.create(x)
}

I am a bit confused that there is no specific access modifier for internal module access (or perhaps I haven't found it yet). Do you have any other suggestions on how to tackle this challenge?

Answer №1

An approach I have found effective in the past involves moving the tryParse helper function inside the Email class as a static method. By doing this, the method gains access to the private constructor of the class:

export class Email {
    static tryParse(x: string): Email | null {
        return new Email(x)
    }

    private constructor(x: string) {
        this._value = x
    }

    private _value: string

    get Value () {
        return this._value
    }
}

// This allows you to create an email instance like:
let x = Email.tryParse("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2a405f425f6a534b42454504494547">[email protected]</a>")

Answer №2

To enhance the encapsulation of your code, consider refraining from exporting the Email class entirely and solely exporting the tryParse function from your module. You can integrate this function within a factory class to streamline the process:

// Module X
class Email {
    private _value: string;

    constructor(x: string) {
        this._value = x;
    }
   
    get Value() {
        return this._value;
    }
}

export class EmailFactory extends Email {
    public static parse(x: string): Email {
        return isValidEmail(x) ? new Email(x) : null;
    }
}

// Module Y
import {EmailFactory} from "ModuleX";

EmailFactory.parse("foo");
// => null
EmailFactory.parse("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="204a554855605941484f4f0e43">[email protected]</a>");
// => Email
new Email("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9bf1eef3edbbaabaeaaff8f7faeffaf620-b6bfbcb3fefff2f4f7f7efe8fc">[email protected]</a>");
// error TS2304: Cannot find name 'Email'.

Answer №3

With the release of Typescript 3.8, a new syntax for exporting types called "export type" was introduced, and it can be implemented in your code.

To use this feature, you can export your helper function normally, but when exporting a class, use export type instead of just export:

// Do not export `Email` here:
class Email { ... }

export function tryParse(x: string): Email | null { ... }

// Instead, export `Email` as a type like this:
export type { Email };

This approach restricts the instantiation of the class from outside the module, while still allowing callers of tryParse to receive a typed Email object that can be used as usual. As noted in the documentation, using this method also prevents the class from being extended externally. Depending on the situation, this may or may not be desirable, but in many cases where internal access control is important, this approach is likely preferred.

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

Reusing methods in Javascript to create child instances without causing circular dependencies

abstract class Fruit { private children: Fruit[] = []; addChild(child: Fruit) { this.children.push(child); } } // Separate files for each subclass // apple.ts class Apple extends Fruit { } // banana.ts class Banana extends Fruit { } ...

WebStorm is unable to detect tsconfig paths

Currently, we are facing an issue with WebStorm identifying some of our named paths as problematic. Despite this, everything compiles correctly with webpack. Here is how our project is structured: apps app1 tsconfig.e2e.json src tests ...

Utilizing interface in NestJS for validating incoming request parameters

My goal is to utilize the interface provided by class-validator in order to validate a specific field in the incoming request body. Here's the interface structure: export enum Fields { Full_Stack_Dev = 'full stack dev', Frontend_Dev = &a ...

Dealing with nullable objects in Typescript: Best practices

Looking for a solution to have a function return an object or null. This is how I am currently addressing it: export interface MyObject { id: string } function test(id) : MyObject | null { if (!id) { return null; } return { ...

Add a calendar icon to the DateRangePicker in React Mui

I am trying to set up a DateRangePicker using Material UI with a calendar icon. Here is an example of how I want it to look: https://i.stack.imgur.com/LnYnY.png After following the API documentation and using this code: components={{ OpenPickerIcon: Cal ...

Utilizing a Dependency Injection container effectively

I am venturing into the world of creating a Node.js backend for the first time after previously working with ASP.NET Core. I am interested in utilizing a DI Container and incorporating controllers into my project. In ASP.NET Core, a new instance of the c ...

Having trouble with data types error in TypeScript while using Next.js?

I am encountering an issue with identifying the data type of the URL that I will be fetching from a REST API. To address this, I have developed a custom hook for usability in my project where I am using Next.js along with TypeScript. Below is the code sni ...

Eliminate duplicate dropdown options in Angular 2 using a filter function

Is there a way to filter reporting results in an Angular 2 dropdown list? I am currently attempting to do so within the *ngFor template but haven't had any success. I will also try using a custom pipe. The data is coming from a JSON array. Specificall ...

Ways to eliminate Typescript assert during the execution of npm run build?

How can I effectively remove Typescript asserts to ensure that a production build generated through the use of npm run build is free of assertions? Your assistance is appreciated ...

Prisma atomic operations encounter errors when attempting to update undefined values

According to the Prisma Typescript definition for atomic operations, we have: export type IntFieldUpdateOperationsInput = { set?: number increment?: number decrement?: number multiply?: number divide?: number } Let's take a look at the Pris ...

What's causing the "* before initialization" error in Vue with TypeScript?

I am encountering an issue with my code where I get the error "Cannot access 'AuthCallback' before initialization" when attempting to call the router function in the AuthCallback component. What could be causing this problem? The desired function ...

Showing object data in TypeScript HTML when the object property starts with a numeral

Below is the function found in the TypeScript file that retrieves data from an API: .ts file getMachineConfigsByid(id) { this.machinesService.getMachineConfigById(id).subscribe((res) => { if (res.status === 'success') { ...

Unable to inject basic service into component

Despite all my efforts, I am struggling to inject a simple service into an Angular2 component. Everything is transpiling correctly, but I keep encountering this error: EXCEPTION: TypeError: Cannot read property 'getSurveyItem' of undefined Even ...

Understanding the functionality of imports within modules imported into Angular

I have been scouring through the documentation trying to understand the functionality of the import statement in JavaScript, specifically within the Angular framework. While I grasp the basic concept that it imports modules from other files containing expo ...

Attention: issue TS18002 has been detected - The 'files' configuration file is currently blank

I'm currently working with TypeScript version 2.1.5.0. My setup includes the grunt-typescript-using-tsconfig plugin, but I'm encountering an error when running the task. The issue seems to be related to the property "files":[] in my tsconfig.jso ...

"Discovering issues with the functionality of Antd Table's search and reset capabilities

I'm currently working on integrating search and reset functions in an Antd table. However, I am encountering an issue with the reset (clearAll) function as it does not revert the table back to its initial state when activated. The search functionality ...

Changing the default font size has no effect on ChartJS

I'm trying to customize the font size for a chart by changing the default value from 40px to 14px. However, when I set Chart.defaults.global.defaultFontSize to 14, the changes don't seem to take effect. Below is the code snippet for reference. An ...

Moving SVG Module

My goal is to create a custom component that can dynamically load and display the content of an SVG file in its template. So far, I have attempted the following approach: icon.component.ts ... @Component({ selector: 'app-icon', templa ...

Using the Angular Slice Pipe to Show Line Breaks or Any Custom Delimiter

Is there a way in Angular Slice Pipe to display a new line or any other delimited separator instead of commas when separating an array like 'Michelle', 'Joe', 'Alex'? Can you choose the separator such as NewLine, / , ; etc? ...

Angular: Connecting template data to different visual presentations

Looking for a solution to display data and map values to another presentation without needing complex ngIf statements or creating multiple components. Check out this sample: https://stackblitz.com/edit/angular-9l1vff The 'vals' variable contain ...