async constructors in js

async constructors in js

javascript async constructor hack typescript

So, have you heard about async constructors? No? Probably because they do not exist (yet!). Nevertheless, recently I found interesting hack in JS which works well with new keyword.

Let's consider Config class as a good example where you could load some configuration from file.


interface AppConfig {
  publicUrl: string
}

class Config {
  constructor(public readonly config: AppConfig) {
		// ...
  }
}

Let's assume that we have some async function fetching file, from fs or url.

const fetchAppConfigFromFile = async (): Promise<AppConfig> => {
  return Promise.resolve({ publicUrl: 'https://dawiid.io/' });
}

So, how can we "feed" Config class with our object? Since in JS (I'm writing examples in TS, but since TS transpiles to JS we can assume no major differences besides typechecking) we can not do something like this:

class Config {
  config: AppConfig;

  async constructor() {
    this.config = await fetchAppConfigFromFile();
		// ...
  }
}

we must delay Config creation, by using for example builder pattern.

Builder:

class Config {
  static async build(): Config {
    const appConfig = await fetchAppConfigFromFile();
    // yes, you can use "this" in static functions even with "new" keyword,
    // it will point to Config class instead of Config instance
    return new this(appConfig); 
  }
	
  // ...
}

const config = await Config.build();

something like this, but there is another way to hack it and use like this:

const config = await new Config();

Have anyone ever said, that you can not return raw promise from constructor instead of instance by itself? 😎 And here comes our hack! You can return promise from constructor, in constructor you can use then api to wait for the results and return the already created instance, so that the client code calling constructor with new will have to wait for it and will receive a new instance! But let's put this into code, and you will understand everything:


const DEFAULT_APP_CONFIG: AppConfig = {
   publicUrl: ''
};

class Config {
    // default value mostly to satisfy TS, because we setting value in "then", after promise fulfillment
    config: AppConfig = DEFAULT_APP_CONFIG;

    constructor() {
        // sadly, I had to put ts-ignore here, TS does not accept (yet!) returning anything else than instance from constructor
        // @ts-ignore
        return fetchAppConfigFromFile().then((config) => {
            this.config = config;
            return this;
        });
    }
}

And now we can do

const config = await new Config();
console.log(config.config.publicUrl); // https://dawiid.io/

Nice, huh? :D

I'm not saying you should rush and put this code in production env, especially when it contains @ts-ignore, but I think that's an interesting, hack-ish option to have under your belt!