Skip to content

feat: introduce NumberRange type#3838

Merged
Shinigami92 merged 2 commits into
nextfrom
feat-add-NumberRange-type
Jun 28, 2026
Merged

feat: introduce NumberRange type#3838
Shinigami92 merged 2 commits into
nextfrom
feat-add-NumberRange-type

Conversation

@Shinigami92

@Shinigami92 Shinigami92 commented May 2, 2026

Copy link
Copy Markdown
Member

the following PR description is claude generated

Summary

Introduces a shared, publicly exported NumberRange interface and uses it to replace the ~40 inline { min: number; max: number } shapes scattered across the module method signatures.

export interface NumberRange {
  /** The minimum value (inclusive). */
  min: number;
  /** The maximum value (inclusive). */
  max: number;
}
import { faker, type NumberRange } from '@faker-js/faker';

const count: NumberRange = { min: 1, max: 10 };

faker.lorem.words(count);
faker.helpers.arrayElements(['a', 'b', 'c'], count);
faker.string.alpha({ length: count });

Refs #1443.

Motivation

  • Shorter, more readable signatures. Method signatures that previously inlined a multi-line number | { min; max } object now read as number | NumberRange.
  • A reusable, importable type for consumers. Users can declare a single NumberRange value and pass it to any range-accepting method, instead of re-declaring the shape inline or reverse-engineering it via Parameters<...>.
  • Deduplication. One canonical definition replaces ~40 copies of the same shape.

Design decision (Option 3)

The discussion weighed several approaches (a single shared type vs. per-concept named types such as WordCountRange, hybrid inline/shared shapes, and intersection types). We are going with Option 3: a single shared NumberRange.

Rationale:

  • For the consumer, NumberRange and a hypothetical WordCountRange are structurally identical - extra per-concept types mostly add mental overhead and import churn.
  • Starting with one type is the forward-compatible choice: a more specific type can be added later non-breakingly if users ask for it, whereas removing redundant types after the fact would be a breaking change.

Tradeoffs

  • The per-field JSDoc that previously lived on each inline min/max (e.g. "The minimum number of words to generate.") is replaced by the generic NumberRange field docs ("The minimum value (inclusive)."). The method-level @param x.min / @param x.max lines are unaffected.
  • Known follow-up: the API-docs generator no longer renders the min/max sub-parameters (and their per-field defaults) for parameters typed as NumberRange. This needs to be addressed separately in the apidoc generation - see the discussion thread.

@Shinigami92 Shinigami92 self-assigned this May 2, 2026
@netlify

netlify Bot commented May 2, 2026

Copy link
Copy Markdown

Deploy Preview for fakerjs ready!

Name Link
🔨 Latest commit 9648390
🔍 Latest deploy log https://app.netlify.com/projects/fakerjs/deploys/6a3f9301cae1e40008ad7108
😎 Deploy Preview https://deploy-preview-3838.fakerjs.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.84%. Comparing base (fd33b72) to head (9648390).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #3838      +/-   ##
==========================================
- Coverage   98.85%   98.84%   -0.01%     
==========================================
  Files         923      923              
  Lines        3224     3216       -8     
  Branches      591      583       -8     
==========================================
- Hits         3187     3179       -8     
  Misses         33       33              
  Partials        4        4              
Files with missing lines Coverage Δ
src/modules/airline/module.ts 100.00% <ø> (ø)
src/modules/date/module.ts 100.00% <ø> (ø)
src/modules/finance/bitcoin.ts 100.00% <ø> (ø)
src/modules/helpers/module.ts 94.84% <100.00%> (ø)
src/modules/lorem/module.ts 100.00% <100.00%> (ø)
src/modules/string/module.ts 100.00% <100.00%> (ø)
src/modules/system/module.ts 100.00% <ø> (ø)
src/modules/word/filter-word-list-by-length.ts 100.00% <100.00%> (ø)
src/modules/word/module.ts 96.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Shinigami92

Copy link
Copy Markdown
Member Author

@ST-DDT @xDivisionByZerox — looking for input on a tradeoff this PR introduces.

Context

This PR replaces ~40 inline { min: number; max: number } shapes with a shared NumberRange interface. As a side effect, the per-site type-level JSDoc on min/max (e.g. "The minimum number of words to generate.") is gone. The method-level @param wordCount.min / @param wordCount.max lines are still there, but the JSDoc that surfaces in VSCode hover on the field itself is now the generic NumberRange description ("The minimum value (inclusive).").

Options

1. Per-concept named ranges

Define small types per use-case, each carrying its own JSDoc:

export interface WordCount extends NumberRange {
  /** The minimum number of words to generate. */
  min: number;
  /** The maximum number of words to generate. */
  max: number;
}
  • ✅ Tooltips recovered, semantically meaningful named types for consumers.
  • ❌ ~10 new exported types; API surface grows.

2. Hybrid: inline shape for surface methods, NumberRange internally

Keep NumberRange for plumbing (helpers.rangeToNumber, helpers.multiple) and revert to inline { /** doc */ min; /** doc */ max } on user-facing methods (lorem.words, string.alpha, …).

  • ✅ Pragmatic — best tooltips where users actually hover.
  • ❌ Inconsistent style across the codebase.

3. Accept it, lean on @param

Ship as-is. TS LSP can surface @param x.min JSDoc on destructured fields in some cases, and the rendered docs site has the info via @param.

  • ✅ Smallest diff, simplest.
  • ❌ Tooltip regression in VSCode for the inline { min, max } form.

4. Re-declare min / max inline on each method, typed as NumberRange-compatible

words(
  wordCount: number | (NumberRange & {
    /** The minimum number of words to generate. */
    min: number;
    /** The maximum number of words to generate. */
    max: number;
  }) = 3
): string;
  • NumberRange stays publicly importable for consumers, no per-concept type explosion, per-method tooltips preserved.
  • ❌ The methods themselves don't gain dedup — each still re-declares min/max with JSDoc; NumberRange is more of a public-API contract than an implementation deduplication.

My take

I lean toward option 4: it keeps NumberRange publicly available for consumers (which is part of the point per #1443), doesn't bloat the API with a specific type for every method, and we still get the tooltip docs at hover sites.

However it extends the tooltip, not overrides it:

Option 1 is also acceptable as a fallback — it gives consumers semantically meaningful types — but I'd rather have one shared NumberRange than ten specific ones.

It does not have the extended JSDoc, but only the specific (which might be a pro-argument for this option)

I don't like option 2: the inconsistency between "surface" and "internal" methods feels like a smell to me.

I don't like option 3: the per-site tooltip docs were an intentional team decision earlier, and I don't want to silently regress that.

Thoughts?

@Shinigami92 Shinigami92 added s: needs decision Needs team/maintainer decision c: feature Request for new feature labels May 2, 2026
@ST-DDT

ST-DDT commented May 2, 2026

Copy link
Copy Markdown
Member

IMO The most important benefit of this change is the shortened method signature => simplified readability.
Most users understand what a range is.

There are a few questions that we need to search answers for first:

A) Do our users understand that a range is {min, max} and NOT [min, max] with minimal effort?

  • If they need to see that it is an object at first glance, then this PR is obsolete.
  • If not and hovering the type is acceptable, then we should decide B+C

B) Do our users have a need for types or do they inline the arguments anyway?
Are our users likely to use our type, instead of declaring their own?
Do we do it for the readability of our signature?

C) Is the context closeness of numberRange.min/max important to our users?
aka do they need to know that it is a WordRange vs a number Range?


  1. IMO this kind of defeats the purpose of this change.
    If needed, our users can create a type for all our parameters themselves (either manually or via Parameters)
    The parameter itself is called wordCount with type NumberRange, IMO it is clear what it does.
    If we go that route anyway, then we should keep the Range as part of the name.

  2. Maybe. IMO this is the smallest and least interruptive. However the benefit is likely negible.
    Depends on (C): If it is important, then this maybe the max extend this PR can go.
    Our users are still able to use our conviencience type. Even if there is no direct link between the two.
    Though I doubt, that anyone will (know of and) use the NumberRange conviencience type then and instead users might define their own copy of the type or access it directly via Parameters[0].
    Also we should keep compatibility with that type in mind when implementing more features.

  3. Maybe.
    Depends on (C): If it is not important, then this is what we should do.
    This would be my recommended way.

  4. No. This negatively impacts readability of the signature. Especially the multiple unions and joins increase mental complexity quite a bit.

  5. "Keep as is in next"
    This would be my next recommendation in line.

Order of preference: 3, 5, 2, 1, 4
(3, 5, 2 are close together in preference, and they mostly depend on the goal of/requirements behind this change)


Diff-Preview

The current apidoc generation process does not display. Min and max params anymore:

Old New
grafik grafik
grafik grafik

Note: when the range currently has a default for either value, then we no longer display it in the jsdocs after this PR is merged.


I don't like option 3: the per-site tooltip docs were an intentional team decision earlier, and I don't want to silently regress that.

Isn't the whole point of this issue/PR to rethink this? If not, what is it?


@Shinigami92

Copy link
Copy Markdown
Member Author

@ST-DDT thanks for the feedback 👍

Isn't the whole point of this issue/PR to rethink this? If not, what is it?

The most valueable feature for consumers would be to use

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

const options: NumberRange = {
//             ^^^^^^^^^^^
  min: 1,
  max: 10
};

//                   vvvvvvv passing typed options
faker.module.method1(options)
faker.module.method2(options)
faker.module.method3(options)

So thinking about this, it would indeed be the best option to use Option 1, even when this is a bit verbose on our side.

@Shinigami92

Shinigami92 commented May 2, 2026

Copy link
Copy Markdown
Member Author

The workaround right now is something very complex like:

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

type NumberRange = Exclude<Parameters<faker.module.method>[0], number>

pseudo code, not tested

@ST-DDT

ST-DDT commented May 2, 2026

Copy link
Copy Markdown
Member

So thinking about this, it would indeed be the best option to use Option 1, even when this is a bit verbose on our side.

Why do you think this distinction is important for our users?

const options: NumberRange = {
  min: 1,
  max: 10
};

faker.module.method1(options)
faker.module.method2(options)
faker.module.method3(options)

vs

const options: WordCountRange = {
  min: 1,
  max: 10
};

faker.module.method1(options)
faker.module.method2(options)
faker.module.method3(options)

IMO both types are identical. And adding more imports/different types just increases the mental costs to remember how they are defined.

@Shinigami92

Copy link
Copy Markdown
Member Author

You are right on the first look, but when in an IDE the second one (WordCountRange) will give the min and max a specific tooltip, instead of a generic.

@ST-DDT ST-DDT added this to the v10.x milestone Jun 22, 2026
@ST-DDT ST-DDT added the needs rebase There is a merge conflict label Jun 25, 2026
@ST-DDT

ST-DDT commented Jun 25, 2026

Copy link
Copy Markdown
Member

IMO in order to move this forward, I would suggest starting with one type and if users complain, add another one.
Since NumberRange === WordCountRange, adding a new type would be breaking, while removing the redundant one would be.

@Shinigami92 Shinigami92 force-pushed the feat-add-NumberRange-type branch from c4a4376 to 9b2f1d3 Compare June 26, 2026 20:43
@Shinigami92 Shinigami92 removed the needs rebase There is a merge conflict label Jun 26, 2026
@Shinigami92

Shinigami92 commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Decision: locking in Option 3 (single shared NumberRange)

After the discussion, we'll proceed with Option 3 — a single shared, publicly exported NumberRange interface — which is what this PR currently implements.

Reasoning (per @ST-DDT):

  • NumberRange and a per-concept type like WordCountRange are structurally identical for consumers; extra named types mostly add import/mental overhead.
  • Starting with one type is forward-compatible: a more specific type can be added later non-breakingly if users actually ask for it, whereas removing redundant types afterwards would be breaking.

This trades away the per-field min/max JSDoc at hover sites in favor of the generic NumberRange field docs, which we accept.

One remaining follow-up before this is ready: the apidoc generator no longer renders the min/max sub-parameters (and their per-field defaults) for NumberRange-typed params. I'll handle that separately.

this comment was fully posted by claude, I told claude to write into a markdown file, but it ignored it and directly posted it 🙁
image

@Shinigami92 Shinigami92 removed the s: needs decision Needs team/maintainer decision label Jun 26, 2026
@Shinigami92 Shinigami92 marked this pull request as ready for review June 26, 2026 21:02
@Shinigami92 Shinigami92 requested a review from a team as a code owner June 26, 2026 21:02
ST-DDT
ST-DDT previously approved these changes Jun 26, 2026
@ST-DDT ST-DDT added the p: 1-normal Nothing urgent label Jun 26, 2026

@xDivisionByZerox xDivisionByZerox left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the NumberRange type is resolved as a type name in the docs, instead of "exploding" the type declaration into more primitive types:

If we want t change that and instead show the underlying "object"-type we would need to resolve it via it's symbol (type => symbol => declaration [type=alias] => type node => text). We do not seem to have tests for parameters so I wasn't able to test this out of the box.


Edit: Should be somewhere here where we would need to change that:

if (resolveAliases && type.isTypeParameter()) {
const text = getTypeText(type.getApparentType(), {
...options,
resolveAliases: true,
});
if (text.text === 'unknown') {
return newSimpleType('any');
}
return text;
}

@ST-DDT

ST-DDT commented Jun 27, 2026

Copy link
Copy Markdown
Member
Here the docs patch (click to expand)
diff --git a/scripts/apidocs/processing/type.ts b/scripts/apidocs/processing/type.ts
index 25ec30c96..2c0310e20 100644
--- a/scripts/apidocs/processing/type.ts
+++ b/scripts/apidocs/processing/type.ts
@@ -91,6 +91,8 @@ export function getTypeText(
           : newSimpleType('string');
 
         return newUnionType([displayType, baseType]);
+      } else if (name === 'NumberRange') {
+        return newSimpleType('{ min: number; max: number }');
       }
 
       const typeParameters = typeArguments.map((t) => getTypeText(t, options));
diff --git a/test/scripts/apidocs/__snapshots__/method.spec.ts.snap b/test/scripts/apidocs/__snapshots__/method.spec.ts.snap
index 7be83ab71..e85e9c37c 100644
--- a/test/scripts/apidocs/__snapshots__/method.spec.ts.snap
+++ b/test/scripts/apidocs/__snapshots__/method.spec.ts.snap
@@ -20,6 +20,7 @@ exports[`method > expected and actual methods are equal 1`] = `
   "methodWithThrows",
   "multiParamMethod",
   "noParamMethod",
+  "numberRangeParamMethod",
   "optionalStringParamMethod",
   "optionsInlineParamMethodWithDefaults",
   "optionsInterfaceParamMethodWithDefaults",
@@ -342,7 +343,7 @@ exports[`method > processMethodLike(literalUnionParamMethod) 1`] = `
         },
         {
           "default": undefined,
-          "description": "Value \`'a'\` or \`'b'\` or an array thereof.",
+          "description": "\`'a'\` or \`'b'\` or an array thereof.",
           "name": "mixed",
           "type": {
             "text": "'a' | 'b' | string | Array<'a' | 'b' | string>",
@@ -395,7 +396,7 @@ exports[`method > processMethodLike(literalUnionParamMethod) 1`] = `
         },
         {
           "default": undefined,
-          "description": "Value \`'a'\` or \`'b'\` or an array thereof.",
+          "description": "\`'a'\` or \`'b'\` or an array thereof.",
           "name": "namedMixed",
           "type": {
             "text": "'a' | 'b' | string | Array<AB | string>",
@@ -983,6 +984,111 @@ exports[`method > processMethodLike(noParamMethod) 1`] = `
 }
 `;
 
+exports[`method > processMethodLike(numberRangeParamMethod) 1`] = `
+{
+  "name": "numberRangeParamMethod",
+  "signatures": [
+    {
+      "deprecated": undefined,
+      "description": "Test with NumberRange.",
+      "examples": [],
+      "experimental": undefined,
+      "parameters": [
+        {
+          "default": undefined,
+          "description": "\`{min: 1, max: 10}\`.",
+          "name": "value",
+          "type": {
+            "text": "{ min: number; max: number }",
+            "type": "simple",
+          },
+        },
+        {
+          "default": undefined,
+          "description": "Array of \`{min: 1, max: 10}\`.",
+          "name": "array",
+          "type": {
+            "text": "Array<{ min: number; max: number }>",
+            "type": "generic",
+            "typeParameters": [
+              {
+                "text": "{ min: number; max: number }",
+                "type": "simple",
+              },
+            ],
+          },
+        },
+        {
+          "default": undefined,
+          "description": "The options parameter.",
+          "name": "options",
+          "type": {
+            "text": "{ ... }",
+            "type": "simple",
+          },
+        },
+        {
+          "default": undefined,
+          "description": "The count parameter.",
+          "name": "options.count",
+          "type": {
+            "text": "{ min: number; max: number }",
+            "type": "simple",
+          },
+        },
+        {
+          "default": undefined,
+          "description": "\`{min: 1, max: 10}\` or an array thereof.",
+          "name": "mixed",
+          "type": {
+            "text": "{ min: number; max: number } | Array<{ min: number; max: number }>",
+            "type": "union",
+            "types": [
+              {
+                "text": "{ min: number; max: number }",
+                "type": "simple",
+              },
+              {
+                "text": "Array<{ min: number; max: number }>",
+                "type": "generic",
+                "typeParameters": [
+                  {
+                    "text": "{ min: number; max: number }",
+                    "type": "simple",
+                  },
+                ],
+              },
+            ],
+          },
+        },
+      ],
+      "remarks": [],
+      "returns": {
+        "text": "string",
+        "type": "simple",
+      },
+      "seeAlsos": [],
+      "signature": "function numberRangeParamMethod(
+    value: NumberRange,
+    array: ReadonlyArray<NumberRange>,
+    options: {
+      /** The count parameter. */
+      count: NumberRange;
+    },
+    mixed: NumberRange | ReadonlyArray<NumberRange>
+  ): string;",
+      "since": "1.0.0",
+      "throws": [],
+    },
+  ],
+  "source": {
+    "column": -1,
+    "filePath": "test/scripts/apidocs/method.example.ts",
+    "line": -1,
+  },
+}
+`;
+
 exports[`method > processMethodLike(optionalStringParamMethod) 1`] = `
 {
   "name": "optionalStringParamMethod",
diff --git a/test/scripts/apidocs/method.example.ts b/test/scripts/apidocs/method.example.ts
index 1651129e9..3aeebb86e 100644
--- a/test/scripts/apidocs/method.example.ts
+++ b/test/scripts/apidocs/method.example.ts
@@ -1,4 +1,4 @@
-import type { Casing, ColorFormat } from '../../../src';
+import type { Casing, ColorFormat, NumberRange } from '../../../src';
 import { FakerError } from '../../../src/errors/faker-error';
 import type { LiteralUnion } from '../../../src/internal/types';
 import type { AlphaNumericChar } from '../../../src/modules/string';
@@ -182,8 +182,8 @@ export class SignatureTest {
    * @param namedValue `'a'` or `'b'`.
    * @param array Array of `'a'` or `'b'`.
    * @param namedArray Array of `'a'` or `'b'`.
-   * @param mixed Value `'a'` or `'b'` or an array thereof.
-   * @param namedMixed Value `'a'` or `'b'` or an array thereof.
+   * @param mixed `'a'` or `'b'` or an array thereof.
+   * @param namedMixed `'a'` or `'b'` or an array thereof.
    *
    * @since 1.0.0
    */
@@ -205,6 +205,34 @@ export class SignatureTest {
     );
   }
 
+  /**
+   * Test with NumberRange.
+   *
+   * @param value `{min: 1, max: 10}`.
+   * @param array Array of `{min: 1, max: 10}`.
+   * @param options The options parameter.
+   * @param options.count `{min: 1, max: 10}`.
+   * @param mixed `{min: 1, max: 10}` or an array thereof.
+   *
+   * @since 1.0.0
+   */
+  numberRangeParamMethod(
+    value: NumberRange,
+    array: ReadonlyArray<NumberRange>,
+    options: {
+      /** The count parameter. */
+      count: NumberRange;
+    },
+    mixed: NumberRange | ReadonlyArray<NumberRange>
+  ): string {
+    return JSON.stringify({
+      value,
+      array,
+      options,
+      mixed,
+    });
+  }
+
   /**
    * Test with a Record parameter.
    *

Feel free to apply now or in a separate PR.

@Shinigami92 Shinigami92 dismissed stale reviews from xDivisionByZerox and ST-DDT via 9648390 June 27, 2026 09:08
@Shinigami92

Copy link
Copy Markdown
Member Author

@ST-DDT example: https://deploy-preview-3838.fakerjs.dev/api/airline.html#flightnumber
well done, good work 👍

@Shinigami92 Shinigami92 requested a review from ST-DDT June 27, 2026 09:14
@ST-DDT ST-DDT added the c: docs Improvements or additions to documentation label Jun 27, 2026
@Shinigami92 Shinigami92 added this pull request to the merge queue Jun 28, 2026
Merged via the queue into next with commit acd5fda Jun 28, 2026
24 checks passed
@Shinigami92 Shinigami92 deleted the feat-add-NumberRange-type branch June 28, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: docs Improvements or additions to documentation c: feature Request for new feature p: 1-normal Nothing urgent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants