Tips on preventing the need for null or undefined checks in JS/Typescript singletons that have an initialization function

Is there a way to streamline the process of handling props in an Object literal that is dynamically initialized only once? I'm looking for a pattern that would eliminate the need for repetitive null/undefined checks and throw errors when certain methods are called before initialization.

I also need to ensure that some methods can be called before the initialization process. Should I consider switching from an object to a class constructor following the singleton pattern? But then, how do I handle external dependencies for the constructor props?

Any suggestions on how I can improve this structure would be greatly appreciated.

interface StuffType {
  x?: string;
  y?: string;
  z?: string;
  fn?: () => void;
  isInit: boolean;
}

const _stuff: StuffType = {
  x: undefined,
  y: undefined,
  z: undefined,
  fn: undefined,
  isInit: false,
};

const isReady = () => _stuff.isInit;

const init = (x: string, y: string, z: string, fn: () => void) => {
  if(_stuff.isInit) return;
  _stuff.x = x;
  _stuff.y = y;
  _stuff.z = z;
  _stuff.fn = fn;
};

const method1 = () => {
  // depends on xyz, fn being defined
  if (!_stuff.isInit) throw new Error('not init');
  _stuff.fn!();
  _stuff.x!;
  _stuff.y!;
  _stuff.z!;
};

// All the following methods would depend on init being done
// would require check on init/throw error/bang operator on props
// const method3 = () =>
// const method4 = () =>
// const method5 = () =>
// const method6 = () =>

const method2 = () => {
  // does not depend on xyz, fn
};

export const Stuff = {
  init,
  isReady,
  method1,
  method2,
  //etc...
};

Stuff.method2(); // fine
Stuff.method1(); // throws here
Stuff.init('x', 'y', 'z', () => console.log('init'));
Stuff.method1(); // does not throw

Answer №1

In order to streamline this process, my suggestion would be to consolidate the data that needs to be initialized into a single separate property. This way, you only have to check if that specific property exists, making the extra isInit property unnecessary.

interface StuffType {
  data?: {
    x: string;
    y: string;
    z: string;
  }
  fn?: () => void;
}
const _stuff: StuffType = {
  fn: undefined,
};
const init = (x: string, y: string, z: string, fn: () => void) => {
  _stuff.data ??= { x, y, z };
};
const method1 = () => {
  const { data } = _stuff;
  if (!data) throw new Error('not init');
  _stuff.fn!();
  data.x;
  data.y;
  data.z;
};

I also want method2 to be able to be called before the init so that I can enqueue some things

There is no indication in the signature of method2 that it relies on any other factors, so there's no need for any real adjustments there; you're free to add

const method2 = () => {
  // does not depend on xyz, fn
};

whatever you desire. (You might consider having a local queue variable within the module, such as const queue = [], which could be updated if method2 is invoked)

Answer №2

Avoiding a partial type where every property is optional individually can be achieved by making them dependent on each other using a discriminated union:

interface Stuff {
  isInit: true;
  x: string;
  y: string;
  z: string;
  fn: () => void;
}

type MaybeStuff = Stuff | { isInit: false };

const _stuff: MaybeStuff = {
  isInit: false,
};

export const method1 = () => {
  if (!_stuff.isInit) throw new Error('not initialised');
  // depends on x, y, z, fn being initialised
  _stuff.fn();
  _stuff.x;
  _stuff.y;
  _stuff.z;
};

An even simpler solution would be to make _stuff a mutable variable initially set to undefined:

interface Stuff {
  x: string;
  y: string;
  z: string;
  fn: () => void;
}

let _stuff: Stuff | undefined; /*
^^^ */

export const isReady = () => _stuff !== undefined;

export function init(x: string, y: string, z: string, fn: () => void) {
  if (_stuff) return; // throw new Error('already initialised');
  stuff = {x, y, z, fn};
};

export function method1() {
  if (!_stuff) throw new Error('not initialised');
  // depends on _stuff being initialised
  _stuff.fn();
  _stuff.x;
  _stuff.y;
  _stuff.z;
}

export function method2() {
  // does not depend on xyz, fn
}

To avoid repeating the if (!_stuff) … line in methods needing the stuff object, create a helper function for them:

function get(): Stuff {
  if (!_stuff) throw new Error('not initialised');
  return _stuff;
}
export function method1() {
  const stuff = get(); // depends on _stuff being initialised
  stuff.fn();
  stuff.x;
  stuff.y;
  stuff.z;
}

export function method2() {
  // does not depend on xyz, fn
}

export function method3() {
  const stuff = get(); // depends on _stuff being initialised
  …
}

[I have] an Object literal that is dynamically initiated only once at some point. This object would serve as a singleton in a lifecycle of the application. Is there a way to make this cleaner?

Creating a proper singleton requires it to be initialised at the start of the application and not during its lifecycle with parameters passed later on. Consider refactoring your module into a class, eliminating the mutable _stuff static variable and the init method. Construct the object when parameter values are available, and let that module provide the instance to the rest of the application (via global variable, singleton pattern, dependency injection, etc.).

Also, ensure that method2 is either a static method of the class if stateless or a method of the module managing initialisation state if it queues tasks until initialisation.

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

I am having trouble getting the hoverOffset feature to work with doughnut charts in vue-charts.js

It appears that no matter what I try, the hoverOffset property is not working on my doughnut charts. Here's the component code: <script> import { Doughnut } from 'vue-chartjs' export default { extends: Doughnut, props: { ch ...

Missing expected property in TypeScript casting operation

I recently came across this intriguing playground example outlining a scenario where I attempted to convert an object literal into an object with specific properties, but encountered unexpected results. class X { y: string; get hi(): string { ...

How to display only a portion of content using UI Bootstrap collapse feature

In my Angular.js application, I currently have a row of buttons that open up new routes to display partial views. Now, I need to modify some of these buttons to trigger a ui.bootstrap Collapse feature with the view inside it. The template code from the ui. ...

Exploring the process of iterating through a JSON response in PHP to extract all values

{ "ver":2, "inputs":[ { "sequence":4294967295, "witness":"", "prev_out":{ "spent":true, "tx_index":372805487, "type":0, "addr":"3AFgA1pHKrk4jFHwzUL1CKgZvXyFSWZfgD", "value":12712, ...

Utilizing vanilla JavaScript or ES6 to extract data from a JSON file

I am currently working on an HTML project where I need to extract data from a JSON file that cannot be modified. I am looking to accomplish this using pure JavaScript or ES6, but I am struggling to make it work. Specifically, I am trying to retrieve a link ...

Executing Ionic function before application shutdown

Does anyone know of a function that can be triggered when the app is about to exit, close, or go into the background? Essentially any event that indicates "the user has stopped using the app"? In my app, I create a 'user log' that keeps track of ...

Tips for transforming a date into a time ago representation

Can someone help me with converting a date field into a "timeago" format using jquery.timeago.js? $("time.timeago").timeago(); var userSpan = document.createElement("span"); userSpan.setAttribute("class", "text-muted"); userSpan.appendChild(document.crea ...

Unable to execute AJAX POST request

https://i.stack.imgur.com/JqG7c.pngI have a JSON dataset like the one below: [ { "Password": "tedd", "Username": "john", "status": true } ] I need to consume this data using a POST method <label for="Username">Username:</label& ...

Having trouble with VS Code on my Linux system - npm error popping up?

protons@protons-HP-EliteBook-840-G3:~/Desktop/NH_Facility_Portal_V0$ sudo npm install [sudo] password for protons: npm notice npm notice New minor version of npm available! 8.12.1 -> 8.13.2 npm notice Changelog: https://github.com/npm/cli/releases/tag ...

"Here's a simple guide to generating a random number within a specified range

I have encountered a specific issue: Within an 8-column grid, I am attempting to randomly place an item with a random span width. While I have successfully managed to position the item and give it a random width, I am struggling with adjusting the width b ...

Discovering the type in Typescript without explicitly defining a type variable for the callback

I am looking for a solution to wrap rxjs subscribe's next callback with my own function: type Handler<T> = (value: T) => void; export function withTryCatch<T>(callback?: Handler<T>): Handler<T> { return (value: T) => ...

Updating React markers dynamically on Google Maps issue

I'm currently developing a transportation app with React and the @react-google-maps/api library. The app includes a map component that displays the real-time location of delivery workers using custom icons. However, I've encountered an issue wher ...

The jQuery validation feature permits entering a code that falls within the range of user1 to user100

Here is an example where the user can only enter a code between 1 and 100. Otherwise, it will return false. var regexCode = /var regexEmail = /^0*(?:[1-9][0-9]?|100)$/; $(document).on('change','#code',function() ...

Methods for reloading the requirejs module

There are two modules: settingmap.js var settingMap = { scWidth : [4000, 6000, 8000], scHeight : [5000, 7000, 9000], bxWidth : [100, 90, 80], bxHeight : [100, 90, 80], totalTime : [50, 40, 30], level : [1, 2, 3], boxColor : [&a ...

Guide to acquiring the webViewLink using Google Drive Api v3?

I'm having trouble locating the webViewLink. The documentation (https://developers.google.com/drive/api/v3/reference/files) states that I should receive this parameter when requesting gapi.client.drive.files.list(). However, I don't even have a c ...

Move the div containing an <audio></audio> tag

Is it possible to drag a div with a width of 200px and an element into a droppable area, and once the div is dropped, have its size change according to the sound duration (e.g. 1px per second)? Check out this example on jsFiddle. Below is the code snipp ...

Deactivate the other RadioButtons within an Asp.net RadioButtonList when one of them is chosen

Is there a way to disable other asp.net radio buttons when one of them is selected? I have three radio buttons, and I want to disable the other two when one is selected. After doing some research, I managed to disable the other two when the third one is se ...

I attempted to implement dialog functionality using material-ui code from the documentation, but for some reason it's not functioning correctly. Can anyone point out what I might

I tried implementing a material-ui dialog feature for React, but I'm facing issues with it. When clicking on the contact button, the handleClickOpen method is not being triggered at all. The contact button is supposed to open the dialog box, and all ...

The new experimental appDir feature in Next.js 13 is failing to display <meta> or <title> tags in the <head> section when rendering on the server

I'm currently experimenting with the new experimental appDir feature in Next.js 13, and I've encountered a small issue. This project is utilizing: Next.js 13 React 18 MUI 5 (styled components using @mui/system @emotion/react @emotion/styled) T ...

Issues with AngularJS dirty validation for radio buttons not being resolved

I have encountered an issue with my form fields. The validation for the email field is working correctly, but the radio button field is not displaying any errors when it should. I am using angular-1.0.8.min.js and I cannot figure out why this is happenin ...