faker icon indicating copy to clipboard operation
faker copied to clipboard

Feat: Instantiate standalone Faker instance, import locales (or extend for custom subclass)

Open ex-nerd opened this issue 3 years ago • 6 comments

Goal:

Create a subclass that extends the Faker class so proprietary fake-data generators can be used side by side with faker's built-in ones.

Problem:

There seems to be a problem with the bound methods getting bound to undefined instead of to the instance. This is likely due to the empty locales option:

  • I didn't see any way to import all locales like Faker's own code does, or even an individual locale (see code example for my attempts)
  • The Faker constructor initializes locale to en, but seems to use an empty object as the default for locales.

Proposed solution(s):

  • Set the default locales in Faker constructor to at least include the en locale so the class can be used via new Faker()
  • Throw an error if new Faker(...) is instantiated with invalid options
  • Fix whatever is preventing the locales from being imported, so users can instantiate their own Faker instances the "long" way.

Demo and other commentary here:

https://stackblitz.com/edit/faker-js-demo-q1dwn6?devtoolsheight=33&file=index.ts

The code is also below, in case that demo gets corrupted:

import { Faker } from '@faker-js/faker';
// I would expect at least one these work:
// import allLocales from '@faker-js/faker/locales';
// import { locales } from '@faker-js/faker/locales';
// import en from '@faker-js/faker/locales/en';
// import { en } from '@faker-js/faker/locales/en';

// This code shows how the bind is failing, resulting in
// "Error: Cannot read properties of undefined (reading 'internet')"
const baseFaker = new Faker(/* {locales: {en}} */);
console.log('baseFaker.internet.email: ', baseFaker.internet.email);
console.log(baseFaker.internet.email());

// Below here is the code I'm actually trying to write.
// I don't think it's relevant to the bug, but I've left
// it here as an example of the end goal and a possible
// solution.

export class MyFake extends Faker {
  readonly customFake = new CustomFake(this);
}

export class CustomFake {
  private readonly fake: MyFake;

  constructor(fake: MyFake) {
    this.fake = fake;
  }

  /* Side note: this syntax seems a lot easier than the
   * bind() methods faker currently uses. */
  public someFakeThing = () => {
    return 'fake data';
  };
}

const myfaker = new MyFake(/* {locales: {en}} */);

// confirm this works
console.log(myfaker.customFake.someFakeThing());

// some debug logs to show values "exist"
// console.log("myfaker: ", myfaker);
// console.log("myfaker.internet: ", myfaker.internet);
console.log('myfaker.internet.email: ', myfaker.internet.email);

// but this fails, presumably because email() is not bound
console.log(myfaker.internet.email());

ex-nerd avatar Feb 07 '22 22:02 ex-nerd

And for posterity, here is a workaround (ugly, but better than nothing):

https://stackblitz.com/edit/faker-js-demo-5j7rsf?devtoolsheight=33&file=index.ts

import { faker, Faker } from '@faker-js/faker';

export class MyFake extends Faker {
	// TODO: make this readonly after https://github.com/faker-js/faker/issues/448
	customFake = new CustomFake(this);
}

export class CustomFake {
	private readonly fake: MyFake;

	constructor(fake: MyFake) {
		this.fake = fake;
	}

	public someFakeThing = () => {
		return "fake data";
	}
}

// @ts-ignore workaround for https://github.com/faker-js/faker/issues/448
const fake: MyFake = faker;
fake.customFake = new CustomFake(fake);

console.log(fake.customFake.someFakeThing());
console.log(fake.internet.email());

Edit: this workaround can be made to work with ts 3.9 by editing one line to say const fake: MyFake & Faker = faker

ex-nerd avatar Feb 07 '22 22:02 ex-nerd

I will try to investigate in next few days into this. Currently I assume it's especially for the esm version, cause we use bundling and only specific entryPoints for this https://github.com/faker-js/faker/blob/f1884becbc108c3ddbad952d879cec0fc5196e08/scripts/bundle.ts#L27-L34 Also we currently do not export single locales one by one.

We also need to export locales from https://github.com/faker-js/faker/blob/main/src/locales/index.ts and add an entryPoint for these files.

Shinigami92 avatar Feb 07 '22 22:02 Shinigami92

@ex-nerd I tried like two hours now and I cannot find a way to archive what you want right now https://github.com/faker-js/faker/pull/455#issuecomment-1032984642

One of the main problems is that we currently want to support cjs and esm

Could you provide a minimal reproduction of a working GitHub repository that works with faker v5.5.3? If this never worked in faker <=5.5.3 then we will try to tackle this feature request in v7 or v8 or later

Shinigami92 avatar Feb 08 '22 19:02 Shinigami92

Could you provide a minimal reproduction of a working GitHub repository that works with faker v5.5.3? If this never worked in faker <=5.5.3 then we will try to tackle this feature request in v7 or v8 or later

v5 typescript support was abysmal (namespace, not class), so as far as I know this definitely wouldn't be possible with that version (though my background is pretty much every language except javascript/typescript so I'm also not the best one to know).

The workaround in my previous comment isn't ideal but seems to work for now.

ex-nerd avatar Feb 08 '22 20:02 ex-nerd

I was also looking for a way to multiple individual faker instances and ran into the same locale problem. This example code

import { Faker } from '@faker-js/faker';

const fakerA = new Faker();
const fakerB = new Faker();

fakerA.seed(100);
fakerB.seed(100);

console.log(fakerA.name.firstName(), fakerA.name.firstName()); // Karianne Marcus
console.log(fakerB.name.firstName(), fakerB.name.firstName()); // Karianne Marcus

throws with this.locales[this.locale] is undefined.

I managed to work around this by using the locales from the default faker export:

import { Faker, faker } from '@faker-js/faker';

const fakerA = new Faker({locales: faker.locales});
const fakerB = new Faker({locales: faker.locales});

fakerA.seed(100);
fakerB.seed(100);

console.log(fakerA.name.firstName(), fakerA.name.firstName()); // Karianne Marcus
console.log(fakerB.name.firstName(), fakerB.name.firstName()); // Karianne Marcus

hpohlmeyer avatar Mar 07 '22 09:03 hpohlmeyer

How about adding a fork() method?

ST-DDT avatar Mar 07 '22 14:03 ST-DDT

faker.fork will be implemented in #1499.

Additionally, custom faker instances should be easier to create in v8. Checkout https://github.com/faker-js/faker/releases/tag/v8.0.0-beta.0.

xDivisionByZerox avatar May 02 '23 08:05 xDivisionByZerox

Team Notice

The top code example works in v8. Updated the code below to make it executable

import { Faker, FakerOptions, en } from "@faker-js/faker";

export class MyFake extends Faker {
  constructor(options: FakerOptions) {
    super(options);
  }

  readonly customFake = new CustomFake(this);
}

export class CustomFake {
  private readonly fake: MyFake;

  constructor(fake: MyFake) {
    this.fake = fake;
  }

  /* Side note: this syntax seems a lot easier than the
   * bind() methods faker currently uses. */
  public someFakeThing = () => {
    return "fake data";
  };
}

const myfaker = new MyFake({ locale: [en] });

// confirm this works
console.log(myfaker.customFake.someFakeThing());

// some debug logs to show values "exist"
// console.log("myfaker: ", myfaker);
// console.log("myfaker.internet: ", myfaker.internet);
console.log("myfaker.internet.email: ", myfaker.internet.email);

// but this fails, presumably because email() is not bound
console.log(myfaker.internet.email());

ST-DDT avatar Aug 31 '23 17:08 ST-DDT