Repository: dubzzz/fast-check
Pull request #1678 (2021-04-01 20:01:47)

✨ Introduce NextArbitrary, new way to define arbitraries
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

Defining new arbitraries from scratch in fast-check might be complex. One of the reason behind this complexity is mostly the fact that generators are responsible to generate a shrinkable value. Other frameworks tend to prefer having an API exposing two methods `generate` and `shrink`. fast-check initially went in that direction but it was impossible to shrink values issued by oneof with such approach... impossible until we came with the concept of `ArbitraryWithContextualShrink`. It somehow showed that having a dedicated `shrink` method was possible but required some more information to be passed.

`NextArbitrary`, replacement of `Arbitrary` in v3 (next major), is the new way to implement such arbitraries. Documentation will be added in coming PRs.

✔️ New feature
❌ Fix an issue
❌ Documentation improvement
❌ Other: *please explain*

(✔️: yes, ❌: no)

## Potential impacts

None


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1678/builds/115786) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit ef2e1e91e6fbafea2f4d6f17862716fc29ceb51a:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-9dlu1)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-74h6b)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=h1) Report
> Merging [#1678](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=desc) (ef2e1e9) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/b40170fa7d9c0d4747f740234990ae30345cf9e6?el=desc) (b40170f) will **decrease** coverage by `2.24%`.
> The diff coverage is `67.43%`.

> :exclamation: Current head ef2e1e9 differs from pull request most recent head 3bf5be5. Consider uploading reports for the commit 3bf5be5 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1678/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main    #1678      +/-   ##
==========================================
- Coverage   97.46%   95.21%   -2.25%     
==========================================
  Files          93       98       +5     
  Lines        3230     3489     +259     
  Branches      624      677      +53     
==========================================
+ Hits         3148     3322     +174     
- Misses         78      163      +85     
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `95.21% <67.43%> (-2.25%)` | :arrow_down: |
| unit-tests-10.x-Linux | `95.21% <67.43%> (-2.25%)` | :arrow_down: |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [...rc/check/arbitrary/definition/ConverterFromNext.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL0NvbnZlcnRlckZyb21OZXh0LnRz) | `42.50% <42.50%> (ø)` | |
| [src/check/arbitrary/definition/ConverterToNext.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL0NvbnZlcnRlclRvTmV4dC50cw==) | `47.61% <47.61%> (ø)` | |
| [src/check/arbitrary/definition/NextArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL05leHRBcmJpdHJhcnkudHM=) | `69.10% <69.10%> (ø)` | |
| [src/check/arbitrary/definition/Converters.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL0NvbnZlcnRlcnMudHM=) | `92.59% <92.59%> (ø)` | |
| [src/check/arbitrary/definition/Arbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL0FyYml0cmFyeS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/check/arbitrary/definition/NextValue.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL05leHRWYWx1ZS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/check/arbitrary/definition/Shrinkable.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9kZWZpbml0aW9uL1Nocmlua2FibGUudHM=) | `100.00% <100.00%> (ø)` | |
| [src/fast-check-default.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree#diff-c3JjL2Zhc3QtY2hlY2stZGVmYXVsdC50cw==) | `100.00% <100.00%> (ø)` | |
| ... and [3 more](https://codecov.io/gh/dubzzz/fast-check/pull/1678/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=footer). Last update [b40170f...3bf5be5](https://codecov.io/gh/dubzzz/fast-check/pull/1678?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/example/package.json b/example/package.json
index 8d0ba7b3..3f8c8872 100644
--- a/example/package.json
+++ b/example/package.json
@@ -6,23 +6,23 @@
     "test": "jest"
   },
   "dependencies": {
-    "@testing-library/dom": "^7.29.6",
-    "@testing-library/jest-dom": "^5.11.8",
-    "@testing-library/react": "^11.2.5",
-    "@testing-library/user-event": "^12.8.0",
+    "@testing-library/dom": "^7.30.3",
+    "@testing-library/jest-dom": "^5.11.10",
+    "@testing-library/react": "^11.2.6",
+    "@testing-library/user-event": "^13.1.1",
     "fast-check": "*",
     "lodash": "^4.17.21",
     "pure-rand": "^4.1.2",
-    "react": "^17.0.1",
-    "react-dom": "^17.0.1"
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2"
   },
   "devDependencies": {
-    "@types/jest": "^26.0.20",
+    "@types/jest": "^26.0.22",
     "@types/lodash": "^4.14.168",
     "@types/react": "^17.0.3",
     "jest": "^26.6.3",
     "ts-jest": "^26.5.4",
-    "typescript": "^4.2.2"
+    "typescript": "^4.2.3"
   },
   "author": "Nicolas DUBIEN <github@dubien.org>",
   "license": "MIT",
diff --git a/example/yarn.lock b/example/yarn.lock
index df6cfd53..ee34df6b 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -494,10 +494,10 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@testing-library/dom@^7.28.1", "@testing-library/dom@^7.29.6":
-  version "7.29.6"
-  resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.6.tgz#eb37844fb431186db7960a7ff6749ea65a19617c"
-  integrity sha512-vzTsAXa439ptdvav/4lsKRcGpAQX7b6wBIqia7+iNzqGJ5zjswApxA6jDAsexrc6ue9krWcbh8o+LYkBXW+GCQ==
+"@testing-library/dom@^7.28.1", "@testing-library/dom@^7.30.3":
+  version "7.30.3"
+  resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.30.3.tgz#779ea9bbb92d63302461800a388a5a890ac22519"
+  integrity sha512-7JhIg2MW6WPwyikH2iL3o7z+FTVgSOd2jqCwTAHqK7Qal2gRRYiUQyURAxtbK9VXm/UTyG9bRihv8C5Tznr2zw==
   dependencies:
     "@babel/code-frame" "^7.10.4"
     "@babel/runtime" "^7.12.5"
@@ -508,10 +508,10 @@
     lz-string "^1.4.4"
     pretty-format "^26.6.2"
 
-"@testing-library/jest-dom@^5.11.8":
-  version "5.11.9"
-  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz#e6b3cd687021f89f261bd53cbe367041fbd3e975"
-  integrity sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==
+"@testing-library/jest-dom@^5.11.10":
+  version "5.11.10"
+  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.10.tgz#1cd90715023e1627f5ed26ab3b38e6f22d77046c"
+  integrity sha512-FuKiq5xuk44Fqm0000Z9w0hjOdwZRNzgx7xGGxQYepWFZy+OYUMOT/wPI4nLYXCaVltNVpU1W/qmD88wLWDsqQ==
   dependencies:
     "@babel/runtime" "^7.9.2"
     "@types/testing-library__jest-dom" "^5.9.1"
@@ -522,18 +522,18 @@
     lodash "^4.17.15"
     redent "^3.0.0"
 
-"@testing-library/react@^11.2.5":
-  version "11.2.5"
-  resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9"
-  integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ==
+"@testing-library/react@^11.2.6":
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b"
+  integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==
   dependencies:
     "@babel/runtime" "^7.12.5"
     "@testing-library/dom" "^7.28.1"
 
-"@testing-library/user-event@^12.8.0":
-  version "12.8.0"
-  resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.8.0.tgz#e07e52d4109a606772f72fa40641b284e8686221"
-  integrity sha512-5+k4U3X6XaFDSBSu6tsD02HVfzuOiPcygQmmYFE2aQQ0e5wRSxWRoU80UH1msa9Q6wuxa0BQsAmwAAAMydcscg==
+"@testing-library/user-event@^13.1.1":
+  version "13.1.1"
+  resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.1.1.tgz#1e011de944cf4d2a917cef6c3046c26389943e24"
+  integrity sha512-B4roX+0mpXKGj8ndd38YoIo3IV9pmTTWxr/2cOke5apTtrNabEUE0KMBccpcAcYlfPcr7uMu+dxeeC3HdXd9qQ==
   dependencies:
     "@babel/runtime" "^7.12.5"
 
@@ -601,10 +601,10 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@*", "@types/jest@^26.0.20":
-  version "26.0.20"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307"
-  integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==
+"@types/jest@*", "@types/jest@^26.0.22":
+  version "26.0.22"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6"
+  integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==
   dependencies:
     jest-diff "^26.0.0"
     pretty-format "^26.0.0"
@@ -2963,24 +2963,24 @@ qs@~6.5.2:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
-react-dom@^17.0.1:
-  version "17.0.1"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
-  integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
+react-dom@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+  integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
-    scheduler "^0.20.1"
+    scheduler "^0.20.2"
 
 react-is@^17.0.1:
   version "17.0.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
   integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
 
-react@^17.0.1:
-  version "17.0.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
-  integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
+react@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+  integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -3178,10 +3178,10 @@ saxes@^5.0.0:
   dependencies:
     xmlchars "^2.2.0"
 
-scheduler@^0.20.1:
-  version "0.20.1"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
-  integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
+scheduler@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+  integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
@@ -3635,10 +3635,10 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@^4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c"
-  integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==
+typescript@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
+  integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
 
 union-value@^1.0.0:
   version "1.0.1"
diff --git a/src/check/arbitrary/definition/Arbitrary.ts b/src/check/arbitrary/definition/Arbitrary.ts
index e2edff1d..c0649416 100644
--- a/src/check/arbitrary/definition/Arbitrary.ts
+++ b/src/check/arbitrary/definition/Arbitrary.ts
@@ -234,3 +234,15 @@ class NoBiasArbitrary<T> extends Arbitrary<T> {
     return this.arb.generate(mrng);
   }
 }
+
+/**
+ * Ensure an instance is an instance of Arbitrary
+ * @param instance - The instance to be checked
+ * @internal
+ */
+export function assertIsArbitrary(instance: Arbitrary<unknown>): void {
+  // TODO - Ideally `: asserts instance is Arbitrary<unknown>` but requires TS 3.7
+  if (typeof instance !== 'object' || instance === null || !('generate' in instance)) {
+    throw new Error('Unexpected value received: not an instance of Arbitrary');
+  }
+}
diff --git a/src/check/arbitrary/definition/ConverterFromNext.ts b/src/check/arbitrary/definition/ConverterFromNext.ts
new file mode 100644
index 00000000..72d68d28
--- /dev/null
+++ b/src/check/arbitrary/definition/ConverterFromNext.ts
@@ -0,0 +1,82 @@
+import { Random } from '../../../random/generator/Random';
+import { Stream } from '../../../stream/Stream';
+import { Arbitrary } from './Arbitrary';
+import { ArbitraryWithContextualShrink } from './ArbitraryWithContextualShrink';
+import { ConverterToNext } from './ConverterToNext';
+import { NextArbitrary } from './NextArbitrary';
+import { NextValue } from './NextValue';
+import { Shrinkable } from './Shrinkable';
+
+const identifier = '__ConverterFromNext__';
+
+/** @internal */
+export class ConverterFromNext<T> extends ArbitraryWithContextualShrink<T> {
+  [identifier] = true;
+  static isConverterFromNext<T>(arb: Arbitrary<T>): arb is ConverterFromNext<T> {
+    return identifier in arb;
+  }
+  private static convertIfNeeded<T>(arb: NextArbitrary<T>): Arbitrary<T> {
+    if (ConverterToNext.isConverterToNext(arb)) return arb.arb;
+    else return new ConverterFromNext(arb);
+  }
+
+  constructor(
+    readonly arb: NextArbitrary<T>,
+    readonly legacyShrunkOnceContext?: unknown,
+    readonly biasFactor: number | undefined = undefined
+  ) {
+    super();
+  }
+
+  generate(mrng: Random): Shrinkable<T, T> {
+    const g = this.arb.generate(mrng, this.biasFactor);
+    return this.toShrinkable(g);
+  }
+  private toShrinkable(v: NextValue<T>): Shrinkable<T, T> {
+    return new Shrinkable(
+      v.value_,
+      () => this.arb.shrink(v.value_, v.context).map((nv) => this.toShrinkable(nv)),
+      () => v.value
+    );
+  }
+
+  contextualShrink(value: T, context?: unknown): Stream<[T, unknown]> {
+    return this.arb.shrink(value, context).map((v) => [v.value_, v.context]);
+  }
+
+  shrunkOnceContext(): unknown {
+    return this.legacyShrunkOnceContext;
+  }
+
+  filter<U extends T>(refinement: (t: T) => t is U): Arbitrary<U>;
+  filter(predicate: (t: T) => boolean): Arbitrary<T>;
+  filter<U extends T>(refinement: (t: T) => t is U): Arbitrary<U> {
+    return ConverterFromNext.convertIfNeeded(this.arb.filter(refinement));
+  }
+
+  map<U>(mapper: (t: T) => U): Arbitrary<U> {
+    return ConverterFromNext.convertIfNeeded(this.arb.map(mapper));
+  }
+
+  chain<U>(fmapper: (t: T) => Arbitrary<U>): Arbitrary<U> {
+    return ConverterFromNext.convertIfNeeded(
+      this.arb.chain((t) => {
+        const fmapped = fmapper(t);
+        if (ConverterFromNext.isConverterFromNext(fmapped)) return fmapped.arb;
+        else return new ConverterToNext(fmapped);
+      })
+    );
+  }
+
+  noShrink(): Arbitrary<T> {
+    return ConverterFromNext.convertIfNeeded(this.arb.noShrink());
+  }
+
+  withBias(freq: number): Arbitrary<T> {
+    return new ConverterFromNext(this.arb, this.legacyShrunkOnceContext, freq);
+  }
+
+  noBias(): Arbitrary<T> {
+    return ConverterFromNext.convertIfNeeded(this.arb.noBias());
+  }
+}
diff --git a/src/check/arbitrary/definition/ConverterToNext.ts b/src/check/arbitrary/definition/ConverterToNext.ts
new file mode 100644
index 00000000..9a440120
--- /dev/null
+++ b/src/check/arbitrary/definition/ConverterToNext.ts
@@ -0,0 +1,74 @@
+import { Random } from '../../../random/generator/Random';
+import { Stream } from '../../../stream/Stream';
+import { Arbitrary } from './Arbitrary';
+import { ConverterFromNext } from './ConverterFromNext';
+import { NextArbitrary } from './NextArbitrary';
+import { NextValue } from './NextValue';
+import { Shrinkable } from './Shrinkable';
+
+const identifier = '__ConverterToNext__';
+
+/** @internal */
+export class ConverterToNext<T> extends NextArbitrary<T> {
+  [identifier] = true;
+  static isConverterToNext<T>(arb: NextArbitrary<T>): arb is ConverterToNext<T> {
+    return identifier in arb;
+  }
+  private static convertIfNeeded<T>(arb: Arbitrary<T>): NextArbitrary<T> {
+    if (ConverterFromNext.isConverterFromNext(arb)) return arb.arb;
+    else return new ConverterToNext(arb);
+  }
+
+  constructor(readonly arb: Arbitrary<T>) {
+    super();
+  }
+
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<T> {
+    const g = biasFactor !== undefined ? this.arb.withBias(biasFactor).generate(mrng) : this.arb.generate(mrng);
+    return new NextValue(g.value_, g, () => g.value);
+  }
+
+  canGenerate(_value: unknown): _value is T {
+    return false;
+  }
+
+  shrink(_value: T, context?: unknown): Stream<NextValue<T>> {
+    if (this.isSafeContext(context)) {
+      return context.shrink().map((s) => new NextValue(s.value_, s, () => s.value));
+    }
+    return Stream.nil();
+  }
+  private isSafeContext(context: unknown): context is Shrinkable<T> {
+    return (
+      context != null && typeof context === 'object' && 'value' in (context as any) && 'shrink' in (context as any)
+    );
+  }
+
+  filter<U extends T>(refinement: (t: T) => t is U): NextArbitrary<U>;
+  filter(predicate: (t: T) => boolean): NextArbitrary<T>;
+  filter<U extends T>(refinement: (t: T) => t is U): NextArbitrary<U> {
+    return ConverterToNext.convertIfNeeded(this.arb.filter(refinement));
+  }
+
+  map<U>(mapper: (t: T) => U): NextArbitrary<U> {
+    return ConverterToNext.convertIfNeeded(this.arb.map(mapper));
+  }
+
+  chain<U>(fmapper: (t: T) => NextArbitrary<U>): NextArbitrary<U> {
+    return ConverterToNext.convertIfNeeded(
+      this.arb.chain((t) => {
+        const fmapped = fmapper(t);
+        if (ConverterToNext.isConverterToNext(fmapped)) return fmapped.arb;
+        else return new ConverterFromNext(fmapped);
+      })
+    );
+  }
+
+  noShrink(): NextArbitrary<T> {
+    return ConverterToNext.convertIfNeeded(this.arb.noShrink());
+  }
+
+  noBias(): NextArbitrary<T> {
+    return ConverterToNext.convertIfNeeded(this.arb.noBias());
+  }
+}
diff --git a/src/check/arbitrary/definition/Converters.ts b/src/check/arbitrary/definition/Converters.ts
new file mode 100644
index 00000000..b1ec70ef
--- /dev/null
+++ b/src/check/arbitrary/definition/Converters.ts
@@ -0,0 +1,60 @@
+import { Arbitrary, assertIsArbitrary } from './Arbitrary';
+import { ArbitraryWithContextualShrink } from './ArbitraryWithContextualShrink';
+import { ConverterFromNext } from './ConverterFromNext';
+import { ConverterToNext } from './ConverterToNext';
+import { assertIsNextArbitrary, NextArbitrary } from './NextArbitrary';
+
+/**
+ * Convert an instance of NextArbitrary to an instance of Arbitrary
+ * @param arb - The instance to be converted
+ * @remarks Since 2.15.0
+ * @public
+ */
+export function convertFromNext<T>(arb: NextArbitrary<T>): Arbitrary<T> {
+  if (ConverterToNext.isConverterToNext(arb)) {
+    return arb.arb;
+  }
+  assertIsNextArbitrary(arb);
+  return new ConverterFromNext(arb);
+}
+
+/**
+ * Convert an instance of NextArbitrary to an instance of ArbitraryWithContextualShrink
+ * @param arb - The instance to be converted
+ * @param legacyShrunkOnceContext - Default context to be returned when shrunk once
+ * @remarks Since 2.15.0
+ * @public
+ */
+export function convertFromNextWithShrunkOnce<T>(
+  arb: NextArbitrary<T>,
+  legacyShrunkOnceContext: unknown
+): ArbitraryWithContextualShrink<T> {
+  if (ConverterToNext.isConverterToNext(arb)) {
+    if (
+      !('contextualShrink' in arb.arb) ||
+      !('contextualShrinkableFor' in arb.arb) ||
+      !('shrunkOnceContext' in arb.arb) ||
+      !('shrink' in arb.arb) ||
+      !('shrinkableFor' in arb.arb)
+    ) {
+      throw new Error('Conversion rejected: Underlying arbitrary is not compatible with ArbitraryWithContextualShrink');
+    }
+    return arb.arb;
+  }
+  assertIsNextArbitrary(arb);
+  return new ConverterFromNext(arb, legacyShrunkOnceContext);
+}
+
+/**
+ * Convert an instance of Arbitrary to an instance of NextArbitrary
+ * @param arb - The instance to be converted
+ * @remarks Since 2.15.0
+ * @public
+ */
+export function convertToNext<T>(arb: Arbitrary<T>): NextArbitrary<T> {
+  if (ConverterFromNext.isConverterFromNext(arb)) {
+    return arb.arb as NextArbitrary<T>;
+  }
+  assertIsArbitrary(arb);
+  return new ConverterToNext(arb);
+}
diff --git a/src/check/arbitrary/definition/NextArbitrary.ts b/src/check/arbitrary/definition/NextArbitrary.ts
new file mode 100644
index 00000000..79b3098c
--- /dev/null
+++ b/src/check/arbitrary/definition/NextArbitrary.ts
@@ -0,0 +1,386 @@
+import { Random } from '../../../random/generator/Random';
+import { Stream } from '../../../stream/Stream';
+import { cloneMethod } from '../../symbols';
+import { NextValue } from './NextValue';
+
+/**
+ * Abstract class able to generate values on type `T`
+ *
+ * @remarks Since 2.15.0
+ * @public
+ */
+export abstract class NextArbitrary<T> {
+  /**
+   * Generate a value of type `T` along with its context (if any)
+   * based on the provided random number generator
+   *
+   * @param mrng - Random number generator
+   * @param biasFactor - If taken into account 1 value over biasFactor must be biased. Either integer value greater or equal to 2 (bias) or undefined (no bias)
+   * @returns Random value of type `T` and its context
+   *
+   * @remarks Since 2.15.0
+   */
+  abstract generate(mrng: Random, biasFactor: number | undefined): NextValue<T>;
+
+  /**
+   * Check if a given value could have been generated
+   * through this instance of arbitrary.
+   *
+   * In general, `canGenerate` is not designed to be called for each `shrink` but rather on very special cases.
+   * Its usage most be restricted to `canGenerate` or in the rare* context of a `shrink` method being called without
+   * any context. In this ill-formed case of `shrink`, `canGenerate` could be used or called if needed.
+   *
+   * *we fall in that case when fast-check is asked to shrink a value that has been provided manually by the user,
+   *  in other words: a value not coming from a call to `generate` or a normal `shrink`.
+   *
+   * @param value - Value to be assessed
+   * @returns `true` if and only if the value could have been generated by this instance
+   *
+   * @remarks Since 2.15.0
+   */
+  abstract canGenerate(value: unknown): value is T;
+
+  /**
+   * Shrink a value of type `T`, may rely on the context previously provided to shrink efficiently
+   *
+   * Must never be called with possibly invalid values.
+   * Consider calling `canGenerate` first if you are not sure of the origin of the value.
+   *
+   * @param value - The value to shrink
+   * @param context - Its associated context (the one returned by generate)
+   * @returns Stream of shrinks for value based on context (if provided)
+   *
+   * @remarks Since 2.15.0
+   */
+  abstract shrink(value: T, context?: unknown): Stream<NextValue<T>>;
+
+  /**
+   * Create another arbitrary by filtering values against `predicate`
+   *
+   * All the values produced by the resulting arbitrary
+   * satisfy `predicate(value) == true`
+   *
+   * Be aware that using filter may highly impact the time required to generate a valid entry
+   *
+   * @example
+   * ```typescript
+   * const integerGenerator: NextArbitrary<number> = ...;
+   * const evenIntegerGenerator: NextArbitrary<number> = integerGenerator.filter(e => e % 2 === 0);
+   * // new NextArbitrary only keeps even values
+   * ```
+   *
+   * @param refinement - Predicate, to test each produced element. Return true to keep the element, false otherwise
+   * @returns New arbitrary filtered using predicate
+   *
+   * @remarks Since 2.15.0
+   */
+  filter<U extends T>(refinement: (t: T) => t is U): NextArbitrary<U>;
+  /**
+   * Create another arbitrary by filtering values against `predicate`
+   *
+   * All the values produced by the resulting arbitrary
+   * satisfy `predicate(value) == true`
+   *
+   * Be aware that using filter may highly impact the time required to generate a valid entry
+   *
+   * @example
+   * ```typescript
+   * const integerGenerator: NextArbitrary<number> = ...;
+   * const evenIntegerGenerator: NextArbitrary<number> = integerGenerator.filter(e => e % 2 === 0);
+   * // new NextArbitrary only keeps even values
+   * ```
+   *
+   * @param predicate - Predicate, to test each produced element. Return true to keep the element, false otherwise
+   * @returns New arbitrary filtered using predicate
+   *
+   * @remarks Since 2.15.0
+   */
+  filter(predicate: (t: T) => boolean): NextArbitrary<T>;
+  filter<U extends T>(refinement: (t: T) => t is U): NextArbitrary<U> {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    return new FilterArbitrary(this, refinement);
+  }
+
+  /**
+   * Create another arbitrary by mapping all produced values using the provided `mapper`
+   * Values produced by the new arbitrary are the result of applying `mapper` value by value
+   *
+   * @example
+   * ```typescript
+   * const rgbChannels: NextArbitrary<{r:number,g:number,b:number}> = ...;
+   * const color: NextArbitrary<string> = rgbChannels.map(ch => `#${(ch.r*65536 + ch.g*256 + ch.b).toString(16).padStart(6, '0')}`);
+   * // transform an NextArbitrary producing {r,g,b} integers into an NextArbitrary of '#rrggbb'
+   * ```
+   *
+   * @param mapper - Map function, to produce a new element based on an old one
+   * @returns New arbitrary with mapped elements
+   *
+   * @remarks Since 2.15.0
+   */
+  map<U>(mapper: (t: T) => U): NextArbitrary<U> {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    return new MapArbitrary(this, mapper);
+  }
+
+  /**
+   * Create another arbitrary by mapping a value from a base Arbirary using the provided `fmapper`
+   * Values produced by the new arbitrary are the result of the arbitrary generated by applying `fmapper` to a value
+   * @example
+   * ```typescript
+   * const arrayAndLimitArbitrary = fc.nat().chain((c: number) => fc.tuple( fc.array(fc.nat(c)), fc.constant(c)));
+   * ```
+   *
+   * @param chainer - Chain function, to produce a new Arbitrary using a value from another Arbitrary
+   * @returns New arbitrary of new type
+   *
+   * @remarks Since 2.15.0
+   */
+  chain<U>(chainer: (t: T) => NextArbitrary<U>): NextArbitrary<U> {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    return new ChainArbitrary(this, chainer);
+  }
+
+  /**
+   * Create another NextArbitrary with no shrink values
+   *
+   * @example
+   * ```typescript
+   * const dataGenerator: NextArbitrary<string> = ...;
+   * const unshrinkableDataGenerator: NextArbitrary<string> = dataGenerator.noShrink();
+   * // same values no shrink
+   * ```
+   *
+   * @returns Create another arbitrary with no shrink values
+   * @remarks Since 2.15.0
+   */
+  noShrink(): NextArbitrary<T> {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    return new NoShrinkArbitrary(this);
+  }
+
+  /**
+   * Create another NextArbitrary that cannot be biased
+   *
+   * @param freq - The biased version will be used one time over freq - if it exists
+   * @remarks Since 2.15.0
+   */
+  noBias(): NextArbitrary<T> {
+    // eslint-disable-next-line @typescript-eslint/no-use-before-define
+    return new NoBiasArbitrary(this);
+  }
+}
+
+/** @internal */
+type ChainArbitraryContext<T, U> = {
+  originalBias: number | undefined;
+  originalValue: T;
+  originalContext: unknown;
+  stoppedForOriginal: boolean;
+  chainedArbitrary: NextArbitrary<U>;
+  chainedContext: unknown;
+  clonedMrng: Random;
+};
+
+/** @internal */
+class ChainArbitrary<T, U> extends NextArbitrary<U> {
+  constructor(readonly arb: NextArbitrary<T>, readonly chainer: (t: T) => NextArbitrary<U>) {
+    super();
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<U> {
+    const clonedMrng = mrng.clone();
+    const src = this.arb.generate(mrng, biasFactor);
+    return this.valueChainer(src, mrng, clonedMrng, biasFactor);
+  }
+  canGenerate(value: unknown): value is U {
+    // TODO Need unchainer
+    return false;
+  }
+  shrink(value: U, context?: unknown): Stream<NextValue<U>> {
+    if (this.isSafeContext(context)) {
+      return (!context.stoppedForOriginal
+        ? this.arb
+            .shrink(context.originalValue, context.originalContext)
+            .map((v) => this.valueChainer(v, context.clonedMrng.clone(), context.clonedMrng, context.originalBias))
+        : Stream.nil<NextValue<U>>()
+      ).join(
+        context.chainedArbitrary.shrink(value, context.chainedContext).map((dst) => {
+          const newContext: ChainArbitraryContext<T, U> = {
+            ...context,
+            chainedContext: dst.context,
+            stoppedForOriginal: true,
+          };
+          return new NextValue(dst.value_, newContext);
+        })
+      );
+    }
+    // TODO Need unchainer
+    return Stream.nil();
+  }
+  private valueChainer(
+    v: NextValue<T>,
+    generateMrng: Random,
+    clonedMrng: Random,
+    biasFactor: number | undefined
+  ): NextValue<U> {
+    const chainedArbitrary = this.chainer(v.value_);
+    const dst = chainedArbitrary.generate(generateMrng, biasFactor);
+    const context: ChainArbitraryContext<T, U> = {
+      originalBias: biasFactor,
+      originalValue: v.value_,
+      originalContext: v.context,
+      stoppedForOriginal: false,
+      chainedArbitrary,
+      chainedContext: dst.context,
+      clonedMrng,
+    };
+    return new NextValue(dst.value_, context);
+  }
+  private isSafeContext(context: unknown): context is ChainArbitraryContext<T, U> {
+    return (
+      context != null &&
+      typeof context === 'object' &&
+      'originalBias' in (context as any) &&
+      'originalValue' in (context as any) &&
+      'originalContext' in (context as any) &&
+      'stoppedForOriginal' in (context as any) &&
+      'chainedArbitrary' in (context as any) &&
+      'chainedContext' in (context as any) &&
+      'clonedMrng' in (context as any)
+    );
+  }
+}
+
+/** @internal */
+type MapArbitraryContext<T> = {
+  originalValue: T;
+  originalContext: unknown;
+};
+
+/** @internal */
+class MapArbitrary<T, U> extends NextArbitrary<U> {
+  readonly bindValueMapper: (v: NextValue<T>) => NextValue<U>;
+  constructor(readonly arb: NextArbitrary<T>, readonly mapper: (t: T) => U) {
+    super();
+    this.bindValueMapper = this.valueMapper.bind(this);
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<U> {
+    const g = this.arb.generate(mrng, biasFactor);
+    return this.valueMapper(g);
+  }
+  canGenerate(value: unknown): value is U {
+    // TODO Need unmapper
+    return false;
+  }
+  shrink(value: U, context?: unknown): Stream<NextValue<U>> {
+    if (this.isSafeContext(context)) {
+      return this.arb.shrink(context.originalValue, context.originalContext).map(this.bindValueMapper);
+    }
+    // TODO Need unmapper
+    return Stream.nil();
+  }
+  private mapperWithCloneIfNeeded(v: NextValue<T>): [U, T] {
+    const sourceValue = v.value;
+    const mappedValue = this.mapper(sourceValue);
+    if (v.hasToBeCloned && mappedValue instanceof Object && Object.isExtensible(mappedValue)) {
+      // WARNING: In case the mapped value is not extensible it will not be extended
+      Object.defineProperty(mappedValue, cloneMethod, { get: () => () => this.mapperWithCloneIfNeeded(v)[0] });
+    }
+    return [mappedValue, sourceValue];
+  }
+  private valueMapper(v: NextValue<T>): NextValue<U> {
+    const [mappedValue, sourceValue] = this.mapperWithCloneIfNeeded(v);
+    const context: MapArbitraryContext<T> = { originalValue: sourceValue, originalContext: v.context };
+    return new NextValue(mappedValue, context);
+  }
+  private isSafeContext(context: unknown): context is MapArbitraryContext<T> {
+    return (
+      context != null &&
+      typeof context === 'object' &&
+      'originalValue' in (context as any) &&
+      'originalContext' in (context as any)
+    );
+  }
+}
+
+/** @internal */
+class FilterArbitrary<T, U extends T> extends NextArbitrary<U> {
+  readonly bindRefinementOnValue: (v: NextValue<T>) => v is NextValue<U>;
+  constructor(readonly arb: NextArbitrary<T>, readonly refinement: (t: T) => t is U) {
+    super();
+    this.bindRefinementOnValue = this.refinementOnValue.bind(this);
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<U> {
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      const g = this.arb.generate(mrng, biasFactor);
+      if (this.refinementOnValue(g)) {
+        return g;
+      }
+    }
+  }
+  canGenerate(value: unknown): value is U {
+    return this.arb.canGenerate(value) && this.refinement(value);
+  }
+  shrink(value: U, context?: unknown): Stream<NextValue<U>> {
+    return this.arb.shrink(value, context).filter(this.bindRefinementOnValue);
+  }
+  private refinementOnValue(v: NextValue<T>): v is NextValue<U> {
+    return this.refinement(v.value);
+  }
+}
+
+/** @internal */
+class NoShrinkArbitrary<T> extends NextArbitrary<T> {
+  constructor(readonly arb: NextArbitrary<T>) {
+    super();
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<T> {
+    return this.arb.generate(mrng, biasFactor);
+  }
+  canGenerate(value: unknown): value is T {
+    return this.arb.canGenerate(value);
+  }
+  shrink(_value: T, _context?: unknown): Stream<NextValue<T>> {
+    return Stream.nil();
+  }
+  noShrink() {
+    return this;
+  }
+}
+
+/** @internal */
+class NoBiasArbitrary<T> extends NextArbitrary<T> {
+  constructor(readonly arb: NextArbitrary<T>) {
+    super();
+  }
+  generate(mrng: Random, _biasFactor: number | undefined): NextValue<T> {
+    return this.arb.generate(mrng, undefined);
+  }
+  canGenerate(value: unknown): value is T {
+    return this.arb.canGenerate(value);
+  }
+  shrink(value: T, context?: unknown): Stream<NextValue<T>> {
+    return this.arb.shrink(value, context);
+  }
+  noBias() {
+    return this;
+  }
+}
+
+/**
+ * Ensure an instance is an isntance of NextArbitrary
+ * @param instance - The instance to be checked
+ * @internal
+ */
+export function assertIsNextArbitrary(instance: NextArbitrary<unknown>): void {
+  // TODO - Ideally `: asserts instance is NextArbitrary<unknown>` but requires TS 3.7
+  if (
+    typeof instance !== 'object' ||
+    instance === null ||
+    !('generate' in instance) ||
+    !('shrink' in instance) ||
+    'shrinkableFor' in instance
+  ) {
+    throw new Error('Unexpected value received: not an instance of NextArbitrary');
+  }
+}
diff --git a/src/check/arbitrary/definition/NextValue.ts b/src/check/arbitrary/definition/NextValue.ts
new file mode 100644
index 00000000..3c5d1a40
--- /dev/null
+++ b/src/check/arbitrary/definition/NextValue.ts
@@ -0,0 +1,65 @@
+import { cloneMethod, hasCloneMethod, WithCloneMethod } from '../../symbols';
+
+/**
+ * A `NextValue<T, TShrink = T>` holds an internal value of type `T`
+ * and its associated context
+ *
+ * @remarks Since 2.15.0
+ * @public
+ */
+export class NextValue<T> {
+  /**
+   * State storing the result of hasCloneMethod
+   * If `true` the value will be cloned each time it gets accessed
+   * @remarks Since 2.15.0
+   */
+  readonly hasToBeCloned: boolean;
+  /**
+   * Flag indicating whether or not the this.value has already been called once
+   * If so, the underlying will be cloned
+   * Only set when hasToBeCloned = true
+   */
+  private readOnce: boolean;
+  /**
+   * Safe value of the shrinkable
+   * Depending on `hasToBeCloned` it will either be `value_` or a clone of it
+   * @remarks Since 2.15.0
+   */
+  readonly value!: T;
+  /**
+   * Internal value of the shrinkable
+   * @remarks Since 2.15.0
+   */
+  readonly value_: T;
+  /**
+   * Context for the generated value
+   * TODO - Do we want to clone it too?
+   * @remarks 2.15.0
+   */
+  readonly context: unknown;
+
+  /**
+   * @param value_ - Internal value of the shrinkable
+   * @param context - Context associated to the generated value (useful for shrink)
+   * @param customGetValue - Limited to internal usages (to ease migration to next), it will be removed on next major
+   */
+  constructor(value_: T, context: unknown = undefined, customGetValue: (() => T) | undefined = undefined) {
+    this.value_ = value_;
+    this.context = context;
+    this.hasToBeCloned = hasCloneMethod(value_);
+    this.readOnce = false;
+    Object.defineProperty(this, 'value', { get: customGetValue !== undefined ? customGetValue : this.getValue });
+  }
+
+  /** @internal */
+  private getValue() {
+    if (this.hasToBeCloned) {
+      if (!this.readOnce) {
+        this.readOnce = true;
+        return this.value_;
+      }
+      return ((this.value_ as unknown) as WithCloneMethod<T>)[cloneMethod]();
+    }
+    return this.value_;
+  }
+}
diff --git a/src/check/arbitrary/definition/Shrinkable.ts b/src/check/arbitrary/definition/Shrinkable.ts
index df5ff0d0..e85bbd8e 100644
--- a/src/check/arbitrary/definition/Shrinkable.ts
+++ b/src/check/arbitrary/definition/Shrinkable.ts
@@ -41,13 +41,18 @@ export class Shrinkable<T, TShrink extends T = T> {
   /**
    * @param value_ - Internal value of the shrinkable
    * @param shrink - Function producing Stream of shrinks associated to value
+   * @param customGetValue - Limited to internal usages (to ease migration to next), it will be removed on next major
    */
-  constructor(value_: T, shrink: () => Stream<Shrinkable<TShrink>> = () => Stream.nil<Shrinkable<TShrink>>()) {
+  constructor(
+    value_: T,
+    shrink: () => Stream<Shrinkable<TShrink>> = () => Stream.nil<Shrinkable<TShrink>>(),
+    customGetValue: (() => T) | undefined = undefined
+  ) {
     this.value_ = value_;
     this.shrink = shrink;
     this.hasToBeCloned = hasCloneMethod(value_);
     this.readOnce = false;
-    Object.defineProperty(this, 'value', { get: this.getValue });
+    Object.defineProperty(this, 'value', { get: customGetValue !== undefined ? customGetValue : this.getValue });
   }
 
   /** @internal */
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index a74b7cc0..cf803d02 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -162,6 +162,9 @@ import {
 } from './check/arbitrary/TypedArrayArbitrary';
 import { sparseArray, SparseArrayConstraints } from './check/arbitrary/SparseArrayArbitrary';
 import { DoubleNextConstraints } from './check/arbitrary/DoubleNextArbitrary';
+import { NextArbitrary } from './check/arbitrary/definition/NextArbitrary';
+import { NextValue } from './check/arbitrary/definition/NextValue';
+import { convertFromNext, convertFromNextWithShrunkOnce, convertToNext } from './check/arbitrary/definition/Converters';
 
 // Explicit cast into string to avoid to have __type: "__PACKAGE_TYPE__"
 /**
@@ -330,10 +333,15 @@ export {
   SchedulerReportItem,
   // extend the framework
   Arbitrary,
+  NextArbitrary,
   ArbitraryWithShrink,
   ArbitraryWithContextualShrink,
   Shrinkable,
+  NextValue,
   cloneMethod,
+  convertFromNext,
+  convertFromNextWithShrunkOnce,
+  convertToNext,
   // print values
   stringify,
   defaultReportMessage,


Test patch:
diff --git a/test/unit/check/arbitrary/definition/Arbitrary.spec.ts b/test/unit/check/arbitrary/definition/Arbitrary.itest.spec.ts
similarity index 100%
rename from test/unit/check/arbitrary/definition/Arbitrary.spec.ts
rename to test/unit/check/arbitrary/definition/Arbitrary.itest.spec.ts
diff --git a/test/unit/check/arbitrary/definition/Arbitrary.utest.spec.ts b/test/unit/check/arbitrary/definition/Arbitrary.utest.spec.ts
new file mode 100644
index 00000000..44142686
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/Arbitrary.utest.spec.ts
@@ -0,0 +1,78 @@
+import { Arbitrary, assertIsArbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+import { ArbitraryWithShrink } from '../../../../../src/check/arbitrary/definition/ArbitraryWithShrink';
+import { Stream } from '../../../../../src/stream/Stream';
+
+describe('Arbitrary', () => {
+  describe('assertIsArbitrary', () => {
+    it('should reject explicitely invalid instances (with missing generate)', () => {
+      // Arrange
+      const fakeArbitrary = {} as Arbitrary<any>;
+
+      // Act / Assert
+      expect(() => assertIsArbitrary(fakeArbitrary)).toThrowErrorMatchingInlineSnapshot(
+        `"Unexpected value received: not an instance of Arbitrary"`
+      );
+    });
+
+    it('should accept instances extending Arbitrary', () => {
+      // Arrange
+      class MyOldArbitrary extends Arbitrary<any> {
+        generate(): Shrinkable<any> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: Arbitrary<any> = new MyOldArbitrary();
+
+      // Act / Assert
+      expect(() => assertIsArbitrary(fakeArbitrary)).not.toThrowError();
+    });
+
+    it('should accept instances extending ArbitraryWithShrink', () => {
+      // Arrange
+      class MyOldArbitraryWithShrink extends ArbitraryWithShrink<any> {
+        generate(): Shrinkable<any> {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<any> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: Arbitrary<any> = new MyOldArbitraryWithShrink();
+
+      // Act / Assert
+      expect(() => assertIsArbitrary(fakeArbitrary)).not.toThrowError();
+    });
+
+    it('should accept instances with the very same API', () => {
+      // Arrange
+      const fakeArbitrary: Arbitrary<any> = {
+        generate() {
+          throw new Error('Not implemented');
+        },
+        filter() {
+          throw new Error('Not implemented');
+        },
+        map() {
+          throw new Error('Not implemented');
+        },
+        chain() {
+          throw new Error('Not implemented');
+        },
+        withBias() {
+          throw new Error('Not implemented');
+        },
+        noBias() {
+          throw new Error('Not implemented');
+        },
+        noShrink() {
+          throw new Error('Not implemented');
+        },
+      };
+
+      // Act / Assert
+      expect(() => assertIsArbitrary(fakeArbitrary)).not.toThrowError();
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/ConverterFromNext.itest.spec.ts b/test/unit/check/arbitrary/definition/ConverterFromNext.itest.spec.ts
new file mode 100644
index 00000000..51ad02ea
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/ConverterFromNext.itest.spec.ts
@@ -0,0 +1,63 @@
+import * as fc from '../../../../../lib/fast-check';
+import * as prand from 'pure-rand';
+
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Random } from '../../../../../src/random/generator/Random';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { ConverterFromNext } from '../../../../../src/check/arbitrary/definition/ConverterFromNext';
+import { ConverterToNext } from '../../../../../src/check/arbitrary/definition/ConverterToNext';
+import { buildNextShrinkTree, renderTree } from '../generic/ShrinkTree';
+import { Stream } from '../../../../../src/stream/Stream';
+
+describe('ConverterFromNext', () => {
+  it('should be revertable using ConverterToNext', () => {
+    fc.assert(
+      fc.property(fc.integer().noShrink(), (seed) => {
+        // Arrange
+        class MyNextArbitrary extends NextArbitrary<[number, number]> {
+          // 2-natural-number tuple arbitrary, with shrinking capabilities
+          generate(mrng: Random): NextValue<[number, number]> {
+            const v1 = mrng.nextInt(0, 20);
+            const v2 = mrng.nextInt(0, 20);
+            const context = mrng.nextInt(0, 1);
+            return new NextValue([v1, v2], context);
+          }
+          canGenerate(_value: unknown): _value is [number, number] {
+            throw new Error('Unused in this context');
+          }
+          shrink(value: [number, number], context?: unknown): Stream<NextValue<[number, number]>> {
+            if (context === undefined) {
+              throw new Error('Unexpected error occurred: Unspecified context encountered');
+            }
+            const safeContext = context as 0 | 1;
+            if (value[safeContext] === 0) {
+              return Stream.nil();
+            }
+            if (context === 0) {
+              return Stream.of(new NextValue([0, value[1]], 1), new NextValue([value[0] - 1, value[1]], 1));
+            } else {
+              return Stream.of(new NextValue([value[0], 0], 0), new NextValue([value[0], value[1] - 1], 0));
+            }
+          }
+        }
+        const originalInstance = new MyNextArbitrary();
+        const transformedInstance = new ConverterToNext(new ConverterFromNext(originalInstance));
+
+        // Act
+        const nextValueFromOriginal = originalInstance.generate(new Random(prand.xorshift128plus(seed)));
+        const treeFromOriginal = renderTree(buildNextShrinkTree(originalInstance, nextValueFromOriginal));
+        const nextValueFromTransformed = transformedInstance.generate(
+          new Random(prand.xorshift128plus(seed)),
+          undefined
+        );
+        const treeFromTransformed = renderTree(buildNextShrinkTree(originalInstance, nextValueFromOriginal));
+
+        // Assert
+        expect(transformedInstance).not.toBe(originalInstance); // not the same instance
+        expect(nextValueFromTransformed.value).toEqual(nextValueFromOriginal.value); // but the same generated value
+        expect(nextValueFromTransformed.context).not.toEqual(nextValueFromOriginal.context); // without the same context (will be rewrapped)
+        expect(treeFromTransformed).toEqual(treeFromOriginal); // but with the same shrinks
+      })
+    );
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/ConverterFromNext.utest.spec.ts b/test/unit/check/arbitrary/definition/ConverterFromNext.utest.spec.ts
new file mode 100644
index 00000000..244d9a1b
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/ConverterFromNext.utest.spec.ts
@@ -0,0 +1,377 @@
+import { convertFromNext } from '../../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+import { Random } from '../../../../../src/random/generator/Random';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { ConverterFromNext } from '../../../../../src/check/arbitrary/definition/ConverterFromNext';
+import { Stream } from '../../../../../src/stream/Stream';
+import * as stubRng from '../../../stubs/generators';
+import { ConverterToNext } from '../../../../../src/check/arbitrary/definition/ConverterToNext';
+
+const mrngNoCall = stubRng.mutable.nocall();
+
+describe('ConverterFromNext', () => {
+  describe('isConverterFromNext', () => {
+    it('should detect its own instances', () => {
+      // Arrange
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate(_mrng: Random): NextValue<number> {
+          throw new Error('Method not implemented.');
+        }
+        canGenerate(_value: unknown): _value is number {
+          throw new Error('Method not implemented');
+        }
+        shrink(_value: number, _context?: unknown): Stream<NextValue<number>> {
+          throw new Error('Method not implemented.');
+        }
+      }
+      const originalInstance = new MyNextArbitrary();
+
+      // Act
+      const transformedInstance = convertFromNext(originalInstance);
+
+      // Assert
+      expect(ConverterFromNext.isConverterFromNext(transformedInstance)).toBe(true);
+    });
+
+    it('should not flag Arbitrary as one of its instances', () => {
+      // Arrange
+      class MyArbitrary extends Arbitrary<number> {
+        generate(_mrng: Random): Shrinkable<number, number> {
+          throw new Error('Method not implemented.');
+        }
+      }
+      const originalInstance = new MyArbitrary();
+
+      // Act / Assert
+      expect(ConverterFromNext.isConverterFromNext(originalInstance)).toBe(false);
+    });
+  });
+
+  describe('generate', () => {
+    it('should be able to generate values using the underlying NextArbitrary', () => {
+      // Arrange
+      const expectedValue = 1;
+      const generate = jest.fn().mockReturnValueOnce(new NextValue(expectedValue));
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (_v: unknown): _v is number => {
+          throw new Error('Unexpected call');
+        };
+        shrink = shrink;
+      }
+      const originalInstance = new MyNextArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall);
+
+      // Assert
+      expect(out.value).toBe(expectedValue);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, undefined);
+      expect(shrink).not.toHaveBeenCalled();
+    });
+
+    it('should be able to generate biased values using the underlying NextArbitrary', () => {
+      // Arrange
+      const expectedValue = 1;
+      const generate = jest.fn().mockReturnValueOnce(new NextValue(expectedValue));
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (_v: unknown): _v is number => {
+          throw new Error('Unexpected call');
+        };
+        shrink = shrink;
+      }
+      const biasedFactor = 42;
+      const originalInstance = new MyNextArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const out = transformedInstance.withBias(biasedFactor).generate(mrngNoCall);
+
+      // Assert
+      expect(out.value).toBe(expectedValue);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, biasedFactor);
+      expect(shrink).not.toHaveBeenCalled();
+    });
+
+    it('should produce Shrinkable values calling shrink with the right context', () => {
+      // Arrange
+      const expectedFirstValue = 1;
+      const expectedFirstContext = Symbol();
+      const expectedShrunkValues = [2, 3, 4];
+      const shrink = jest
+        .fn<Stream<NextValue<number>>, any[]>()
+        .mockReturnValueOnce(Stream.of(...expectedShrunkValues.map((v) => new NextValue(v))));
+      const generate = jest.fn().mockReturnValueOnce(new NextValue(expectedFirstValue, expectedFirstContext));
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (_v: unknown): _v is number => {
+          throw new Error('Unexpected call');
+        };
+        shrink = shrink;
+      }
+      const originalInstance = new MyNextArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall);
+      const outShrink = out.shrink();
+
+      // Assert
+      expect([...outShrink].map((v) => v.value)).toEqual(expectedShrunkValues);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledWith(expectedFirstValue, expectedFirstContext);
+    });
+
+    it('should produce Shrinkable values calling shrink with the right context even deeper', () => {
+      // Arrange
+      const expectedShrunkValuesCall1 = [
+        [2, Symbol()],
+        [3, Symbol()],
+        [4, Symbol()],
+      ] as const;
+      const expectedShrunkValuesCall2 = [
+        [5, Symbol()],
+        [6, Symbol()],
+      ] as const;
+      const shrink = jest
+        .fn<Stream<NextValue<number>>, any[]>()
+        .mockReturnValueOnce(Stream.of(...expectedShrunkValuesCall1.map((v) => new NextValue(v[0], v[1]))))
+        .mockReturnValueOnce(Stream.of(...expectedShrunkValuesCall2.map((v) => new NextValue(v[0], v[1]))));
+      const generate = jest.fn().mockReturnValueOnce(new NextValue(1, Symbol()));
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (_v: unknown): _v is number => {
+          throw new Error('Unexpected call');
+        };
+        shrink = shrink;
+      }
+      const originalInstance = new MyNextArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall);
+      const outShrinkLvl1 = out.shrink();
+      const outShrinkLvl2 = outShrinkLvl1.getNthOrLast(1)!.shrink();
+
+      // Assert
+      expect([...outShrinkLvl2].map((v) => v.value)).toEqual(expectedShrunkValuesCall2.map((v) => v[0]));
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledTimes(2);
+      expect(shrink).toHaveBeenLastCalledWith(expectedShrunkValuesCall1[1][0], expectedShrunkValuesCall1[1][1]);
+    });
+  });
+
+  describe('filter', () => {
+    it('should call filter directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const filter = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        filter = filter;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const filteredInstance = new MyNextArbitrary();
+      filter.mockReturnValue(filteredInstance);
+      const predicate = () => true;
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.filter(predicate);
+
+      // Assert
+      expect(filter).toHaveBeenCalledTimes(1);
+      expect(filter).toHaveBeenCalledWith(predicate);
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterFromNext<number>).arb).toBe(filteredInstance);
+    });
+
+    it('should unwrap instances of ConverterFromNext returned by filter', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const filter = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        filter = filter;
+      }
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const filteredInstanceOld = new MyArbitrary();
+      filter.mockReturnValue(new ConverterToNext(filteredInstanceOld));
+      const predicate = () => true;
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.filter(predicate);
+
+      // Assert
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(false);
+      expect(transformedInstanceFiltered).toBe(filteredInstanceOld);
+    });
+  });
+
+  describe('map', () => {
+    it('should call map directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const map = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        map = map;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const filteredInstance = new MyNextArbitrary();
+      map.mockReturnValue(filteredInstance);
+      const mapper = () => 0;
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.map(mapper);
+
+      // Assert
+      expect(map).toHaveBeenCalledTimes(1);
+      expect(map).toHaveBeenCalledWith(mapper);
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterFromNext<number>).arb).toBe(filteredInstance);
+    });
+
+    it('should unwrap instances of ConverterFromNext returned by map', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const map = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        map = map;
+      }
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const mappedInstanceOld = new MyArbitrary();
+      map.mockReturnValue(new ConverterToNext(mappedInstanceOld));
+      const mapper = () => 0;
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceMapped = transformedInstance.map(mapper);
+
+      // Assert
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceMapped)).toBe(false);
+      expect(transformedInstanceMapped).toBe(mappedInstanceOld);
+    });
+  });
+
+  describe('chain', () => {
+    it('should call chain directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const chain = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        chain = chain;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const chainedInstance = new MyNextArbitrary();
+      const outChainedInstance = new MyNextArbitrary();
+      const outChainedInstanceNext = new ConverterFromNext(outChainedInstance);
+      chain.mockReturnValue(chainedInstance);
+      const chainer = () => outChainedInstanceNext;
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.chain(chainer);
+
+      // Assert
+      expect(chain).toHaveBeenCalledTimes(1);
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterFromNext<number>).arb).toBe(chainedInstance);
+    });
+  });
+
+  describe('noShrink', () => {
+    it('should call noShrink directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const noShrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        noShrink = noShrink;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const noShrinkInstance = new MyNextArbitrary();
+      noShrink.mockReturnValue(noShrinkInstance);
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.noShrink();
+
+      // Assert
+      expect(noShrink).toHaveBeenCalledTimes(1);
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterFromNext<number>).arb).toBe(noShrinkInstance);
+    });
+  });
+
+  describe('noBias', () => {
+    it('should call noBias directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const noBias = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        noBias = noBias;
+      }
+      const originalInstance = new MyNextArbitrary();
+      const noShrinkInstance = new MyNextArbitrary();
+      noBias.mockReturnValue(noShrinkInstance);
+
+      // Act
+      const transformedInstance = new ConverterFromNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.noBias();
+
+      // Assert
+      expect(noBias).toHaveBeenCalledTimes(1);
+      expect(ConverterFromNext.isConverterFromNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterFromNext<number>).arb).toBe(noShrinkInstance);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/ConverterToNext.utest.spec.ts b/test/unit/check/arbitrary/definition/ConverterToNext.utest.spec.ts
new file mode 100644
index 00000000..e0235af3
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/ConverterToNext.utest.spec.ts
@@ -0,0 +1,340 @@
+import { convertToNext } from '../../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+import { Random } from '../../../../../src/random/generator/Random';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { ConverterToNext } from '../../../../../src/check/arbitrary/definition/ConverterToNext';
+import { Stream } from '../../../../../src/stream/Stream';
+import { ConverterFromNext } from '../../../../../src/check/arbitrary/definition/ConverterFromNext';
+import * as stubRng from '../../../stubs/generators';
+
+const mrngNoCall = stubRng.mutable.nocall();
+
+describe('ConverterToNext', () => {
+  describe('isConverterToNext', () => {
+    it('should detect its own instances', () => {
+      // Arrange
+      class MyArbitrary extends Arbitrary<number> {
+        generate(_mrng: Random): Shrinkable<number, number> {
+          throw new Error('Method not implemented.');
+        }
+      }
+      const originalInstance = new MyArbitrary();
+
+      // Act
+      const transformedInstance = convertToNext(originalInstance);
+
+      // Assert
+      expect(ConverterToNext.isConverterToNext(transformedInstance)).toBe(true);
+    });
+
+    it('should not flag NextArbitrary as one of its instances', () => {
+      // Arrange
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate(_mrng: Random): NextValue<number> {
+          throw new Error('Method not implemented.');
+        }
+        canGenerate(_value: unknown): _value is number {
+          throw new Error('Method not implemented.');
+        }
+        shrink(_value: number, _context?: unknown): Stream<NextValue<number>> {
+          throw new Error('Method not implemented.');
+        }
+      }
+      const originalInstance = new MyNextArbitrary();
+
+      // Act / Assert
+      expect(ConverterToNext.isConverterToNext(originalInstance)).toBe(false);
+    });
+  });
+
+  describe('generate', () => {
+    it('should be able to generate values using the underlying Arbitrary', () => {
+      // Arrange
+      const expectedValue = 1;
+      const generate = jest.fn().mockReturnValueOnce(new Shrinkable(expectedValue));
+      const withBias = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        withBias = withBias;
+      }
+      const originalInstance = new MyArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(out.value).toBe(expectedValue);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall);
+      expect(withBias).not.toHaveBeenCalled();
+    });
+
+    it('should be able to generate biased values using the underlying Arbitrary', () => {
+      // Arrange
+      const expectedBiasedFactor = 42;
+      const expectedValue = 1;
+      const generate = jest.fn().mockReturnValueOnce(new Shrinkable(expectedValue));
+      const withBias = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        withBias = withBias;
+      }
+      const originalInstance = new MyArbitrary();
+      withBias.mockReturnValue(originalInstance);
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall, expectedBiasedFactor);
+
+      // Assert
+      expect(out.value).toBe(expectedValue);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall);
+      expect(withBias).toHaveBeenCalledTimes(1);
+      expect(withBias).toHaveBeenCalledWith(expectedBiasedFactor);
+    });
+  });
+
+  describe('shrink', () => {
+    it('should be able to shrink values using the returned Shrinkable', () => {
+      // Arrange
+      const expectedShrunkValues = [2, 3, 4];
+      const shrink = jest
+        .fn<Stream<Shrinkable<number, number>>, any[]>()
+        .mockReturnValueOnce(Stream.of(...expectedShrunkValues.map((v) => new Shrinkable(v))));
+      const generate = jest.fn().mockReturnValueOnce(new Shrinkable(1, shrink));
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+      }
+      const originalInstance = new MyArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall, undefined);
+      const outShrink = transformedInstance.shrink(out.value, out.context);
+
+      // Assert
+      expect([...outShrink].map((v) => v.value)).toEqual(expectedShrunkValues);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(shrink).toHaveBeenCalledTimes(1);
+    });
+
+    it('should be able to shrink values using the returned Shrinkable even deeper', () => {
+      // Arrange
+      const expectedShrunkValues = [5, 6];
+      const shrinkLvl2 = jest
+        .fn<Stream<Shrinkable<number, number>>, any[]>()
+        .mockReturnValueOnce(Stream.of(...expectedShrunkValues.map((v) => new Shrinkable(v))));
+      const shrinkLvl1 = jest
+        .fn<Stream<Shrinkable<number, number>>, any[]>()
+        .mockReturnValueOnce(Stream.of(...[2, 3, 4].map((v) => new Shrinkable(v, shrinkLvl2))));
+      const generate = jest.fn().mockReturnValueOnce(new Shrinkable(1, shrinkLvl1));
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+      }
+      const originalInstance = new MyArbitrary();
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const out = transformedInstance.generate(mrngNoCall, undefined);
+      const outShrinkLvl1 = transformedInstance.shrink(out.value, out.context);
+      const firstShrunkValue = outShrinkLvl1.getNthOrLast(0)!;
+      const outShrinkLvl2 = transformedInstance.shrink(firstShrunkValue.value, firstShrunkValue.context);
+
+      // Assert
+      expect([...outShrinkLvl2].map((v) => v.value)).toEqual(expectedShrunkValues);
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(shrinkLvl1).toHaveBeenCalledTimes(1);
+      expect(shrinkLvl2).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('filter', () => {
+    it('should call filter directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const filter = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        filter = filter;
+      }
+      const originalInstance = new MyArbitrary();
+      const filteredInstance = new MyArbitrary();
+      filter.mockReturnValue(filteredInstance);
+      const predicate = () => true;
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.filter(predicate);
+
+      // Assert
+      expect(filter).toHaveBeenCalledTimes(1);
+      expect(filter).toHaveBeenCalledWith(predicate);
+      expect(ConverterToNext.isConverterToNext(transformedInstanceFiltered)).toBe(true);
+      expect((transformedInstanceFiltered as ConverterToNext<number>).arb).toBe(filteredInstance);
+    });
+
+    it('should unwrap instances of ConverterFromNext returned by filter', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const filter = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        filter = filter;
+      }
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+      }
+      const originalInstance = new MyArbitrary();
+      const filteredInstanceNext = new MyNextArbitrary();
+      filter.mockReturnValue(new ConverterFromNext(filteredInstanceNext));
+      const predicate = () => true;
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceFiltered = transformedInstance.filter(predicate);
+
+      // Assert
+      expect(ConverterToNext.isConverterToNext(transformedInstanceFiltered)).toBe(false);
+      expect(transformedInstanceFiltered).toBe(filteredInstanceNext);
+    });
+  });
+
+  describe('map', () => {
+    it('should call map directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const map = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        map = map;
+      }
+      const originalInstance = new MyArbitrary();
+      const mappedInstance = new MyArbitrary();
+      map.mockReturnValue(mappedInstance);
+      const mapper = () => 0;
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceMapped = transformedInstance.map(mapper);
+
+      // Assert
+      expect(map).toHaveBeenCalledTimes(1);
+      expect(map).toHaveBeenCalledWith(mapper);
+      expect(ConverterToNext.isConverterToNext(transformedInstanceMapped)).toBe(true);
+      expect((transformedInstanceMapped as ConverterToNext<number>).arb).toBe(mappedInstance);
+    });
+
+    it('should unwrap instances of ConverterFromNext returned by map', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      const map = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        map = map;
+      }
+      class MyNextArbitrary extends NextArbitrary<number> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (v: unknown) => v is number;
+        shrink = shrink;
+        map = map;
+      }
+      const originalInstance = new MyArbitrary();
+      const mappedInstanceNext = new MyNextArbitrary();
+      map.mockReturnValue(new ConverterFromNext(mappedInstanceNext));
+      const mapper = () => 0;
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceMapped = transformedInstance.map(mapper);
+
+      // Assert
+      expect(ConverterToNext.isConverterToNext(transformedInstanceMapped)).toBe(false);
+      expect(transformedInstanceMapped).toBe(mappedInstanceNext);
+    });
+  });
+
+  describe('chain', () => {
+    it('should call map directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const chain = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        chain = chain;
+      }
+      const originalInstance = new MyArbitrary();
+      const chainedInstance = new MyArbitrary();
+      const outChainedInstance = new MyArbitrary();
+      const outChainedInstanceNext = new ConverterToNext(outChainedInstance);
+      chain.mockReturnValue(chainedInstance);
+      const chainer = () => outChainedInstanceNext;
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceMapped = transformedInstance.chain(chainer);
+
+      // Assert
+      expect(chain).toHaveBeenCalledTimes(1);
+      expect(ConverterToNext.isConverterToNext(transformedInstanceMapped)).toBe(true);
+      expect((transformedInstanceMapped as ConverterToNext<number>).arb).toBe(chainedInstance);
+    });
+  });
+
+  describe('noShrink', () => {
+    it('should call noShrink directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const noShrink = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        noShrink = noShrink;
+      }
+      const originalInstance = new MyArbitrary();
+      const noShrinkInstance = new MyArbitrary();
+      noShrink.mockReturnValue(noShrinkInstance);
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceNoShrink = transformedInstance.noShrink();
+
+      // Assert
+      expect(noShrink).toHaveBeenCalledTimes(1);
+      expect(ConverterToNext.isConverterToNext(transformedInstanceNoShrink)).toBe(true);
+      expect((transformedInstanceNoShrink as ConverterToNext<number>).arb).toBe(noShrinkInstance);
+    });
+  });
+
+  describe('noBias', () => {
+    it('should call noBias directly on the passed instance and convert its result', () => {
+      // Arrange
+      const generate = jest.fn();
+      const noBias = jest.fn();
+      class MyArbitrary extends Arbitrary<number> {
+        generate = generate;
+        noBias = noBias;
+      }
+      const originalInstance = new MyArbitrary();
+      const noBiasInstance = new MyArbitrary();
+      noBias.mockReturnValue(noBiasInstance);
+
+      // Act
+      const transformedInstance = new ConverterToNext(originalInstance);
+      const transformedInstanceNoBias = transformedInstance.noBias();
+
+      // Assert
+      expect(noBias).toHaveBeenCalledTimes(1);
+      expect(ConverterToNext.isConverterToNext(transformedInstanceNoBias)).toBe(true);
+      expect((transformedInstanceNoBias as ConverterToNext<number>).arb).toBe(noBiasInstance);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/Converters.itest.spec.ts b/test/unit/check/arbitrary/definition/Converters.itest.spec.ts
new file mode 100644
index 00000000..9698555b
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/Converters.itest.spec.ts
@@ -0,0 +1,92 @@
+import {
+  convertFromNext,
+  convertFromNextWithShrunkOnce,
+  convertToNext,
+} from '../../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+import { Random } from '../../../../../src/random/generator/Random';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { ArbitraryWithContextualShrink } from '../../../../../src/check/arbitrary/definition/ArbitraryWithContextualShrink';
+import { Stream } from '../../../../../src/stream/Stream';
+
+describe('Converters', () => {
+  it('should return the original instance of Arbitrary when calling convertFromNext(convertToNext)', () => {
+    // Arrange
+    class MyArbitrary extends Arbitrary<number> {
+      generate(_mrng: Random): Shrinkable<number, number> {
+        throw new Error('Method not implemented.');
+      }
+    }
+    const originalInstance = new MyArbitrary();
+
+    // Act
+    const transformedInstance = convertFromNext(convertToNext(originalInstance));
+
+    // Assert
+    expect(transformedInstance).toBe(originalInstance);
+  });
+
+  it('should return the original instance of NextArbitrary when calling convertToNext(convertFromNext)', () => {
+    // Arrange
+    class MyNextArbitrary extends NextArbitrary<number> {
+      generate(_mrng: Random): NextValue<number> {
+        throw new Error('Method not implemented.');
+      }
+      canGenerate(_value: unknown): _value is number {
+        throw new Error('Method not implemented.');
+      }
+      shrink(_value: number, _context?: unknown): Stream<NextValue<number>> {
+        throw new Error('Method not implemented.');
+      }
+    }
+    const originalInstance = new MyNextArbitrary();
+
+    // Act
+    const transformedInstance = convertToNext(convertFromNext(originalInstance));
+
+    // Assert
+    expect(transformedInstance).toBe(originalInstance);
+  });
+
+  it('should return the original instance of ArbitraryWithContextualShrink when calling convertFromNextWithShrunkOnce(convertToNext)', () => {
+    // Arrange
+    class MyArbitraryWithContextualShrink extends ArbitraryWithContextualShrink<number> {
+      generate(_mrng: Random): Shrinkable<number, number> {
+        throw new Error('Method not implemented.');
+      }
+      contextualShrink(): Stream<[number, unknown]> {
+        throw new Error('Method not implemented.');
+      }
+      shrunkOnceContext(): unknown {
+        return 0;
+      }
+    }
+    const originalInstance = new MyArbitraryWithContextualShrink();
+
+    // Act
+    const transformedInstance = convertFromNextWithShrunkOnce(
+      convertToNext(originalInstance),
+      originalInstance.shrunkOnceContext()
+    );
+
+    // Assert
+    expect(transformedInstance).toBe(originalInstance);
+  });
+
+  it('should reject conversion convertFromNextWithShrunkOnce(convertToNext) if original instance was not ArbitraryWithContextualShrink', () => {
+    // Arrange
+    class MyArbitrary extends Arbitrary<number> {
+      generate(_mrng: Random): Shrinkable<number, number> {
+        throw new Error('Method not implemented.');
+      }
+    }
+    const originalInstance = new MyArbitrary();
+
+    // Act / Assert
+    expect(() => convertFromNextWithShrunkOnce(convertToNext(originalInstance), 0)).toThrowErrorMatchingInlineSnapshot(
+      `"Conversion rejected: Underlying arbitrary is not compatible with ArbitraryWithContextualShrink"`
+    );
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/Converters.utest.spec.ts b/test/unit/check/arbitrary/definition/Converters.utest.spec.ts
new file mode 100644
index 00000000..e1f2a280
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/Converters.utest.spec.ts
@@ -0,0 +1,75 @@
+import {
+  convertFromNext,
+  convertFromNextWithShrunkOnce,
+  convertToNext,
+} from '../../../../../src/check/arbitrary/definition/Converters';
+import { mocked } from 'ts-jest/utils';
+
+import { Arbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+
+import * as ArbitraryMock from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import * as NextArbitraryMock from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import * as ConverterFromNextMock from '../../../../../src/check/arbitrary/definition/ConverterFromNext';
+import * as ConverterToNextMock from '../../../../../src/check/arbitrary/definition/ConverterToNext';
+jest.mock('../../../../../src/check/arbitrary/definition/Arbitrary');
+jest.mock('../../../../../src/check/arbitrary/definition/NextArbitrary');
+jest.mock('../../../../../src/check/arbitrary/definition/ConverterFromNext');
+jest.mock('../../../../../src/check/arbitrary/definition/ConverterToNext');
+
+describe('Converters', () => {
+  it('should check the validity of the Arbitrary when performing a conversion with convertToNext', () => {
+    // Arrange
+    const { assertIsArbitrary } = mocked(ArbitraryMock);
+    const { ConverterFromNext } = mocked(ConverterFromNextMock);
+    const { ConverterToNext } = mocked(ConverterToNextMock);
+    const { isConverterFromNext } = mocked(ConverterFromNext);
+    isConverterFromNext.mockReturnValueOnce(false);
+    const originalInstance = {} as Arbitrary<any>;
+
+    // Act
+    convertToNext(originalInstance);
+
+    // Assert
+    expect(isConverterFromNext).toHaveBeenCalledWith(originalInstance);
+    expect(assertIsArbitrary).toHaveBeenCalledWith(originalInstance);
+    expect(ConverterToNext).toHaveBeenCalledWith(originalInstance);
+  });
+
+  it('should check the validity of the Arbitrary when performing a conversion with convertFromNext', () => {
+    // Arrange
+    const { assertIsNextArbitrary } = mocked(NextArbitraryMock);
+    const { ConverterToNext } = mocked(ConverterToNextMock);
+    const { ConverterFromNext } = mocked(ConverterFromNextMock);
+    const { isConverterToNext } = mocked(ConverterToNext);
+    isConverterToNext.mockReturnValueOnce(false);
+    const originalInstance = {} as NextArbitrary<any>;
+
+    // Act
+    convertFromNext(originalInstance);
+
+    // Assert
+    expect(isConverterToNext).toHaveBeenCalledWith(originalInstance);
+    expect(assertIsNextArbitrary).toHaveBeenCalledWith(originalInstance);
+    expect(ConverterFromNext).toHaveBeenCalledWith(originalInstance);
+  });
+
+  it('should check the validity of the Arbitrary when performing a conversion with convertFromNextWithShrunkOnce', () => {
+    // Arrange
+    const { assertIsNextArbitrary } = mocked(NextArbitraryMock);
+    const { ConverterToNext } = mocked(ConverterToNextMock);
+    const { ConverterFromNext } = mocked(ConverterFromNextMock);
+    const { isConverterToNext } = mocked(ConverterToNext);
+    isConverterToNext.mockReturnValueOnce(false);
+    const originalInstance = {} as NextArbitrary<any>;
+    const legacyShrunkOnceContext = Symbol();
+
+    // Act
+    convertFromNextWithShrunkOnce(originalInstance, legacyShrunkOnceContext);
+
+    // Assert
+    expect(isConverterToNext).toHaveBeenCalledWith(originalInstance);
+    expect(assertIsNextArbitrary).toHaveBeenCalledWith(originalInstance);
+    expect(ConverterFromNext).toHaveBeenCalledWith(originalInstance, legacyShrunkOnceContext);
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/NextArbitrary.itest.spec.ts b/test/unit/check/arbitrary/definition/NextArbitrary.itest.spec.ts
new file mode 100644
index 00000000..db403845
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/NextArbitrary.itest.spec.ts
@@ -0,0 +1,263 @@
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { cloneMethod, hasCloneMethod } from '../../../../../src/check/symbols';
+import { Random } from '../../../../../src/random/generator/Random';
+import { Stream } from '../../../../../src/stream/Stream';
+
+import * as stubRng from '../../../stubs/generators';
+import { buildNextShrinkTree, renderTree, walkTree } from '../generic/ShrinkTree';
+
+const mrngNoCall = stubRng.mutable.nocall();
+
+describe('NextArbitrary', () => {
+  describe('map', () => {
+    it('should produce the right shrinking tree', () => {
+      // Arrange
+      class MyArbitrary extends NextArbitrary<number> {
+        generate(_mrng: Random): NextValue<number> {
+          return new NextValue(10, { step: 2 });
+        }
+        canGenerate(value: unknown): value is number {
+          throw new Error('No call expected in current scenario');
+        }
+        shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+          if (typeof context !== 'object' || context === null || !('step' in context)) {
+            throw new Error('Invalid context for MyArbitrary');
+          }
+          const currentStep = (context as { step: number }).step;
+          const nextStep = currentStep + 1;
+          return Stream.of(
+            ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+            ...(value - 1 >= 0 ? [new NextValue(value - 1, { step: nextStep })] : [])
+          );
+        }
+      }
+      const arb = new MyArbitrary().map((n) => String(n));
+
+      // Act
+      const g = arb.generate(mrngNoCall, undefined);
+      const renderedTree = renderTree(buildNextShrinkTree(arb, g)).join('\n');
+
+      // Assert
+      expect(renderedTree).toMatchInlineSnapshot(`
+        "\\"10\\"
+        ├> \\"8\\"
+        |  ├> \\"5\\"
+        |  |  ├> \\"1\\"
+        |  |  |  └> \\"0\\"
+        |  |  └> \\"4\\"
+        |  |     └> \\"3\\"
+        |  |        └> \\"2\\"
+        |  |           └> \\"1\\"
+        |  |              └> \\"0\\"
+        |  └> \\"7\\"
+        |     ├> \\"3\\"
+        |     |  └> \\"2\\"
+        |     |     └> \\"1\\"
+        |     |        └> \\"0\\"
+        |     └> \\"6\\"
+        |        ├> \\"1\\"
+        |        |  └> \\"0\\"
+        |        └> \\"5\\"
+        |           └> \\"4\\"
+        |              └> \\"3\\"
+        |                 └> \\"2\\"
+        |                    └> \\"1\\"
+        |                       └> \\"0\\"
+        └> \\"9\\"
+           ├> \\"6\\"
+           |  ├> \\"2\\"
+           |  |  └> \\"1\\"
+           |  |     └> \\"0\\"
+           |  └> \\"5\\"
+           |     ├> \\"0\\"
+           |     └> \\"4\\"
+           |        └> \\"3\\"
+           |           └> \\"2\\"
+           |              └> \\"1\\"
+           |                 └> \\"0\\"
+           └> \\"8\\"
+              ├> \\"4\\"
+              |  └> \\"3\\"
+              |     └> \\"2\\"
+              |        └> \\"1\\"
+              |           └> \\"0\\"
+              └> \\"7\\"
+                 ├> \\"2\\"
+                 |  └> \\"1\\"
+                 |     └> \\"0\\"
+                 └> \\"6\\"
+                    ├> \\"0\\"
+                    └> \\"5\\"
+                       └> \\"4\\"
+                          └> \\"3\\"
+                             └> \\"2\\"
+                                └> \\"1\\"
+                                   └> \\"0\\""
+      `);
+    });
+
+    it('should preserve cloneable capabilities during both generation and shrinking process', () => {
+      // Arrange
+      type MyArbitraryOutput = { value: number };
+      class MyArbitrary extends NextArbitrary<MyArbitraryOutput> {
+        private create(value: number): MyArbitraryOutput {
+          const complexInstance = { value, [cloneMethod]: () => this.create(value) };
+          return complexInstance;
+        }
+        generate(_mrng: Random): NextValue<MyArbitraryOutput> {
+          return new NextValue(this.create(10));
+        }
+        canGenerate(value: unknown): value is MyArbitraryOutput {
+          throw new Error('No call expected in current scenario');
+        }
+        shrink(v: MyArbitraryOutput, _context?: unknown): Stream<NextValue<MyArbitraryOutput>> {
+          const value = v.value;
+          return Stream.of(
+            ...(value - 2 >= 0 ? [new NextValue(this.create(value - 2))] : []),
+            ...(value - 1 >= 0 ? [new NextValue(this.create(value - 1))] : [])
+          );
+        }
+      }
+      const seenInstances = new Set<unknown>();
+      const arb = new MyArbitrary().map((out) => ({ stringValue: String(out.value), counter: 0 }));
+
+      // Act
+      const g = arb.generate(mrngNoCall, undefined);
+      const shrinkTree = buildNextShrinkTree(arb, g);
+      const shrinkTreeB = buildNextShrinkTree(arb, g);
+
+      // Assert
+      const visit = (instance: { counter: number }) => {
+        expect(hasCloneMethod(instance)).toBe(true); // clone method should be provided
+        expect(instance.counter).toBe(0); // counter should be unique per cloned instance
+        expect(seenInstances.has(instance)).toBe(false); // each instance must appear only once in the tree
+        instance.counter += 1;
+        seenInstances.add(instance);
+      };
+      walkTree(shrinkTree, visit);
+      walkTree(shrinkTreeB, visit);
+    });
+  });
+
+  describe('chain', () => {
+    it('should produce the right shrinking tree', () => {
+      // Arrange
+      class MyArbitrary extends NextArbitrary<number> {
+        generate(_mrng: Random): NextValue<number> {
+          return new NextValue(5, { step: 2 });
+        }
+        canGenerate(value: unknown): value is number {
+          throw new Error('No call expected in current scenario');
+        }
+        shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+          if (typeof context !== 'object' || context === null || !('step' in context)) {
+            throw new Error('Invalid context for MyArbitrary');
+          }
+          const currentStep = (context as { step: number }).step;
+          if (value - currentStep < 0) {
+            return Stream.nil();
+          }
+          const nextStep = currentStep + 1;
+          return Stream.of(new NextValue(value - currentStep, { step: nextStep }));
+        }
+      }
+      class MyChainedArbitrary extends NextArbitrary<number[]> {
+        constructor(readonly size: number, readonly value: number) {
+          super();
+        }
+        generate(_mrng: Random): NextValue<number[]> {
+          return new NextValue(Array(this.size).fill(this.value), {
+            size: this.size,
+            value: this.value,
+          });
+        }
+        canGenerate(value: unknown): value is number[] {
+          throw new Error('No call expected in current scenario');
+        }
+        shrink(value: number[], context?: unknown): Stream<NextValue<number[]>> {
+          if (typeof context !== 'object' || context === null || !('size' in context) || !('value' in context)) {
+            throw new Error('Invalid context for MyChainedArbitrary');
+          }
+          const currentContext = context as { size: number; value: number };
+          if (currentContext.size === 0) {
+            return Stream.nil();
+          }
+          return Stream.of(
+            ...(currentContext.value === value[0]
+              ? [new NextValue(Array(currentContext.size).fill(0), currentContext)]
+              : []),
+            ...(value.length > 1 ? [new NextValue([value[0]], currentContext)] : [])
+          );
+        }
+      }
+      const arb = new MyArbitrary().chain((n) => new MyChainedArbitrary(n, n));
+
+      // Act
+      const g = arb.generate(mrngNoCall, undefined);
+      const renderedTree = renderTree(buildNextShrinkTree(arb, g)).join('\n');
+
+      // Assert
+      expect(renderedTree).toMatchInlineSnapshot(`
+        "[5,5,5,5,5]
+        ├> [3,3,3]
+        |  ├> []
+        |  ├> [0,0,0]
+        |  |  └> [0]
+        |  └> [3]
+        |     └> [0,0,0]
+        |        └> [0]
+        ├> [0,0,0,0,0]
+        |  └> [0]
+        └> [5]
+           └> [0,0,0,0,0]
+              └> [0]"
+      `);
+    });
+  });
+
+  describe('filter', () => {
+    it('should produce the right shrinking tree', () => {
+      // Arrange
+      class MyArbitrary extends NextArbitrary<number> {
+        generate(_mrng: Random): NextValue<number> {
+          return new NextValue(10, { step: 3 });
+        }
+        canGenerate(value: unknown): value is number {
+          throw new Error('No call expected in current scenario');
+        }
+        shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+          if (typeof context !== 'object' || context === null || !('step' in context)) {
+            throw new Error('Invalid context for MyArbitrary');
+          }
+          const currentStep = (context as { step: number }).step;
+          const nextStep = currentStep + 1;
+          return Stream.of(
+            ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+            ...(value - 2 >= 0 ? [new NextValue(value - 2, { step: nextStep })] : []),
+            ...(value - 1 >= 0 ? [new NextValue(value - 1, { step: nextStep })] : [])
+          );
+        }
+      }
+      const arb = new MyArbitrary().filter((n) => n % 2 === 0);
+
+      // Act
+      const g = arb.generate(mrngNoCall, undefined);
+      const renderedTree = renderTree(buildNextShrinkTree(arb, g)).join('\n');
+
+      // Assert
+      expect(renderedTree).toMatchInlineSnapshot(`
+        "10
+        └> 8
+           ├> 4
+           |  └> 2
+           |     └> 0
+           └> 6
+              └> 4
+                 └> 2
+                    └> 0"
+      `);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/NextArbitrary.utest.spec.ts b/test/unit/check/arbitrary/definition/NextArbitrary.utest.spec.ts
new file mode 100644
index 00000000..517ae7d3
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/NextArbitrary.utest.spec.ts
@@ -0,0 +1,765 @@
+import { Arbitrary } from '../../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary, assertIsNextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
+import { ArbitraryWithShrink } from '../../../../../src/check/arbitrary/definition/ArbitraryWithShrink';
+import { Stream } from '../../../../../src/stream/Stream';
+import * as stubRng from '../../../stubs/generators';
+import { cloneMethod, hasCloneMethod } from '../../../../../src/check/symbols';
+import { Random } from '../../../../../src/random/generator/Random';
+
+const mrngNoCall = stubRng.mutable.nocall();
+
+describe('NextArbitrary', () => {
+  describe('assertIsNextArbitrary', () => {
+    it('should reject explicitely invalid instances (with missing generate)', () => {
+      // Arrange
+      const fakeArbitrary = {} as NextArbitrary<any>;
+
+      // Act / Assert
+      expect(() => assertIsNextArbitrary(fakeArbitrary)).toThrowErrorMatchingInlineSnapshot(
+        `"Unexpected value received: not an instance of NextArbitrary"`
+      );
+    });
+
+    it('should reject basic instances of Arbitrary (legacy)', () => {
+      // Arrange
+      class MyOldArbitrary extends Arbitrary<any> {
+        generate(): Shrinkable<any> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary = (new MyOldArbitrary() as unknown) as NextArbitrary<any>;
+
+      // Act / Assert
+      expect(() => assertIsNextArbitrary(fakeArbitrary)).toThrowErrorMatchingInlineSnapshot(
+        `"Unexpected value received: not an instance of NextArbitrary"`
+      );
+    });
+
+    it('should reject basic instances of ArbitraryWithShrink (legacy)', () => {
+      // Arrange
+      class MyOldArbitraryWithShrink extends ArbitraryWithShrink<any> {
+        generate(): Shrinkable<any> {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<any> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary = (new MyOldArbitraryWithShrink() as unknown) as NextArbitrary<any>;
+
+      // Act / Assert
+      expect(() => assertIsNextArbitrary(fakeArbitrary)).toThrowErrorMatchingInlineSnapshot(
+        `"Unexpected value received: not an instance of NextArbitrary"`
+      );
+    });
+
+    it('should accept instances extending NextArbitrary', () => {
+      // Arrange
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate(): NextValue<any> {
+          throw new Error('Not implemented.');
+        }
+        canGenerate(value: unknown): value is any {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<NextValue<any>> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: NextArbitrary<any> = new MyNextArbitrary();
+
+      // Act / Assert
+      expect(() => assertIsNextArbitrary(fakeArbitrary)).not.toThrowError();
+    });
+
+    it('should accept instances with the very same API', () => {
+      // Arrange
+      const fakeArbitrary: NextArbitrary<any> = {
+        generate() {
+          throw new Error('Not implemented');
+        },
+        canGenerate(v): v is any {
+          throw new Error('Not implemented');
+        },
+        shrink() {
+          throw new Error('Not implemented');
+        },
+        filter() {
+          throw new Error('Not implemented');
+        },
+        map() {
+          throw new Error('Not implemented');
+        },
+        chain() {
+          throw new Error('Not implemented');
+        },
+        noBias() {
+          throw new Error('Not implemented');
+        },
+        noShrink() {
+          throw new Error('Not implemented');
+        },
+      };
+
+      // Act / Assert
+      expect(() => assertIsNextArbitrary(fakeArbitrary)).not.toThrowError();
+    });
+  });
+
+  describe('filter', () => {
+    it('should filter the values produced by the original arbitrary on generate', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choice1 = new NextValue(1, Symbol());
+      const choice2 = new NextValue(2, Symbol());
+      const choice3 = new NextValue(3, Symbol());
+      const choice4 = new NextValue(4, Symbol());
+      generate
+        .mockReturnValueOnce(choice1)
+        .mockReturnValueOnce(choice2)
+        .mockReturnValueOnce(choice3)
+        .mockReturnValueOnce(choice4);
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().filter((v) => v % 3 === 0);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g).toBe(choice3); // just returning the first NextValue that fits
+      expect(generate).toHaveBeenNthCalledWith(3, mrngNoCall, expectedBiasFactor); // same Random not cloned
+    });
+
+    it('should filter the values produced by the original arbitrary on shrink', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const valueToShrink = 5;
+      const contextToShrink = Symbol();
+      const choice1 = new NextValue(1, Symbol());
+      const choice2 = new NextValue(2, Symbol());
+      const choice3 = new NextValue(3, Symbol());
+      const choice4 = new NextValue(4, Symbol());
+      const choice5 = new NextValue(6, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(choice1, choice2, choice3, choice4, choice5));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().filter((v) => v % 3 === 0);
+      const shrinks = arb.shrink(valueToShrink, contextToShrink);
+
+      // Assert
+      expect([...shrinks]).toEqual([choice3, choice5]); // just keeping values fitting the predicate
+      expect(shrink).toHaveBeenCalledWith(valueToShrink, contextToShrink);
+    });
+
+    it.each`
+      canGenerateOutput | predicateOutput | expected
+      ${false}          | ${false}        | ${false}
+      ${false}          | ${true}         | ${false}
+      ${false}          | ${false}        | ${false}
+      ${true}           | ${true}         | ${true}
+    `(
+      'should check underlying arbitrary then predicate to know if the value could have been generated',
+      ({ canGenerateOutput, predicateOutput, expected }) => {
+        // Arrange
+        const requestedValue = Symbol();
+        const generate = jest.fn();
+        const canGenerate = jest.fn();
+        const shrink = jest.fn();
+        const predicate = jest.fn();
+        class MyNextArbitrary extends NextArbitrary<any> {
+          generate = generate;
+          canGenerate = (canGenerate as any) as (value: unknown) => value is any;
+          shrink = shrink;
+        }
+        canGenerate.mockReturnValueOnce(canGenerateOutput);
+        predicate.mockReturnValueOnce(predicateOutput);
+
+        // Act
+        const arb = new MyNextArbitrary().filter(predicate);
+        const out = arb.canGenerate(requestedValue);
+
+        // Assert
+        expect(out).toBe(expected);
+        expect(canGenerate).toHaveBeenCalledWith(requestedValue);
+        if (canGenerateOutput) {
+          expect(predicate).toHaveBeenCalledWith(requestedValue);
+        } else {
+          expect(predicate).not.toHaveBeenCalledWith(requestedValue);
+          expect(predicate).not.toHaveBeenCalled();
+        }
+      }
+    );
+  });
+
+  describe('map', () => {
+    it('should map the values produced by the original arbitrary on generate', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choice = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choice);
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map((v) => String(v));
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g.value).toBe(String(choice.value)); // value has been mapped
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+    });
+
+    it('should preserve cloneable capabilities for mapped values on generate', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choice = new NextValue({ source: 1, [cloneMethod]: () => choice.value_ }, Symbol());
+      generate.mockReturnValueOnce(choice);
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map((v) => ({ stringValue: String(v.source) }));
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g.value.stringValue).toBe(String(choice.value.source)); // value has been mapped
+      expect(g.hasToBeCloned).toBe(true); // clone has been preserved on NextValue
+      expect(hasCloneMethod(g.value)).toBe(true); // clone has been preserved on the instance
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+    });
+
+    it('should properly shrink output of generate by calling back shrink with the right context', () => {
+      // Arrange
+      const expectedBiasFactor = 42;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const source = new NextValue(69, Symbol());
+      generate.mockReturnValueOnce(source);
+      const choice1 = new NextValue(1, Symbol());
+      const choice2 = new NextValue(2, Symbol());
+      const choice3 = new NextValue(3, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(choice1, choice2, choice3));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map((v) => String(v));
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinks = arb.shrink(g.value, g.context);
+
+      // Assert
+      expect([...shrinks].map((s) => s.value)).toEqual(['1', '2', '3']); // just mapping values
+      expect(shrink).toHaveBeenCalledWith(source.value, source.context);
+    });
+
+    it('should properly shrink output of shrink by calling back shrink with the right context', () => {
+      // Arrange
+      const expectedBiasFactor = 42;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const source = new NextValue(69, Symbol());
+      generate.mockReturnValueOnce(source);
+      const choice1 = new NextValue(1, Symbol());
+      const choice2 = new NextValue(2, Symbol());
+      const choice3 = new NextValue(3, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(choice1, choice2, choice3));
+      const choice21 = new NextValue(21, Symbol());
+      const choice22 = new NextValue(22, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(choice21, choice22));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map((v) => String(v));
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinksGen1 = arb.shrink(g.value, g.context);
+      const mappedChoice2 = shrinksGen1.getNthOrLast(1)!;
+      const shrinksGen2 = arb.shrink(mappedChoice2.value, mappedChoice2.context);
+
+      // Assert
+      expect([...shrinksGen2].map((s) => s.value)).toEqual(['21', '22']); // just mapping values
+      expect(shrink).toHaveBeenCalledWith(source.value, source.context);
+      expect(shrink).toHaveBeenCalledWith(choice2.value, choice2.context);
+    });
+
+    it('should preserve cloneable capabilities for mapped values on shrink', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const source = new NextValue({ source: 1, [cloneMethod]: () => source.value_ }, Symbol());
+      generate.mockReturnValueOnce(source);
+      const choice1 = new NextValue({ source: 2, [cloneMethod]: () => choice1.value_ }, Symbol());
+      const choice2 = new NextValue({ source: 2, [cloneMethod]: () => choice2.value_ }, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(choice1, choice2));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map((v) => ({ stringValue: String(v.source) }));
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinks = arb.shrink(g.value, g.context);
+      const shrinksValues = [...shrinks];
+
+      // Assert
+      expect(shrinksValues).toHaveLength(2);
+      expect(shrinksValues[0].hasToBeCloned).toBe(true); // clone has been preserved on NextValue
+      expect(shrinksValues[1].hasToBeCloned).toBe(true);
+      expect(hasCloneMethod(shrinksValues[0].value)).toBe(true); // clone has been preserved on the instance
+      expect(hasCloneMethod(shrinksValues[1].value)).toBe(true);
+    });
+
+    it('should always return false for canGenerate when not provided any unmapper function', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (value: unknown) => value is any;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map(() => '');
+      const out = arb.canGenerate('');
+
+      // Assert
+      expect(out).toBe(false);
+      expect(canGenerate).not.toHaveBeenCalled();
+    });
+
+    it('should return empty stream when shrinking without any context and not provided any unmapper function', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (value: unknown) => value is any;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().map(() => '');
+      const shrinks = arb.shrink('');
+
+      // Assert
+      expect([...shrinks]).toHaveLength(0);
+      expect(shrink).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('chain', () => {
+    it('should chain the values produced by the original arbitrary on generate', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choiceRoot = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choiceRoot);
+      const generateChained = jest.fn();
+      const canGenerateChained = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrinkChained = jest.fn();
+      const choiceChained = new NextValue(50, Symbol());
+      generateChained.mockReturnValueOnce(choiceChained);
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+      class MyNextChainedArbitrary extends NextArbitrary<any> {
+        generate = generateChained;
+        canGenerate = canGenerateChained;
+        shrink = shrinkChained;
+      }
+      const chainer = jest.fn();
+      chainer.mockReturnValueOnce(new MyNextChainedArbitrary());
+
+      // Act
+      const arb = new MyNextArbitrary().chain(chainer);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g.value).toBe(choiceChained.value); // value has been chained
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor); // the two calls to generate
+      expect(generateChained).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor); // they share the same Random
+      expect(chainer).toHaveBeenCalledWith(choiceRoot.value); // chainer was called with output of generate
+    });
+
+    it('should properly shrink output of generate by calling back shrink with the right context', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choiceRoot = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choiceRoot);
+      const shrinkRoot1 = new NextValue(10, Symbol());
+      const shrinkRoot2 = new NextValue(11, Symbol());
+      const shrinkRoot3 = new NextValue(15, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(shrinkRoot1, shrinkRoot2, shrinkRoot3));
+      const generateChained = jest.fn();
+      const canGenerateChained = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrinkChained = jest.fn();
+      const choiceChained = new NextValue(50, Symbol());
+      const choiceShrink1Chained = new NextValue(58, Symbol()); // chain will be called for each sub-shrink of root
+      const choiceShrink2Chained = new NextValue(57, Symbol());
+      const choiceShrink3Chained = new NextValue(16, Symbol());
+      generateChained
+        .mockReturnValueOnce(choiceChained)
+        .mockReturnValueOnce(choiceShrink1Chained)
+        .mockReturnValueOnce(choiceShrink2Chained)
+        .mockReturnValueOnce(choiceShrink3Chained);
+      const shrinkChained1 = new NextValue(25, Symbol());
+      const shrinkChained2 = new NextValue(51, Symbol());
+      shrinkChained.mockReturnValueOnce(Stream.of(shrinkChained1, shrinkChained2));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+      class MyNextChainedArbitrary extends NextArbitrary<any> {
+        generate = generateChained;
+        canGenerate = canGenerateChained;
+        shrink = shrinkChained;
+      }
+      const chainer = jest.fn();
+      chainer.mockReturnValue(new MyNextChainedArbitrary());
+
+      // Act
+      const arb = new MyNextArbitrary().chain(chainer);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinks = arb.shrink(g.value, g.context);
+
+      // Assert
+      expect([...shrinks].map((v) => v.value)).toEqual([
+        choiceShrink1Chained.value,
+        choiceShrink2Chained.value,
+        choiceShrink3Chained.value,
+        shrinkChained1.value,
+        shrinkChained2.value,
+      ]); // shrink of source chained, then the one of original chained
+      expect(generateChained).toHaveBeenNthCalledWith(4, expect.any(Random), expectedBiasFactor); // sub-sequent calls re-use the original bias
+      expect(chainer).toHaveBeenCalledWith(choiceRoot.value); // original call
+      expect(chainer).toHaveBeenCalledWith(shrinkRoot1.value); // chained during shrink
+      expect(chainer).toHaveBeenCalledWith(shrinkRoot2.value); // chained during shrink
+      expect(chainer).toHaveBeenCalledWith(shrinkRoot3.value); // chained during shrink
+    });
+
+    it('should properly shrink output of shrink by calling back shrink with the right context', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choiceRoot = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choiceRoot);
+      const shrinkRoot1 = new NextValue(10, Symbol());
+      const shrinkRoot2 = new NextValue(11, Symbol()); // will not be iterated (getNthOrLast(0))
+      const shrinkRoot3 = new NextValue(15, Symbol()); // will not be iterated (getNthOrLast(0))
+      shrink.mockReturnValueOnce(Stream.of(shrinkRoot1, shrinkRoot2, shrinkRoot3));
+      const shrinkRoot11 = new NextValue(310, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(shrinkRoot11));
+      const generateChained = jest.fn();
+      const canGenerateChained = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrinkChained = jest.fn();
+      const choiceChained = new NextValue(50, Symbol());
+      const choiceShrink1Chained = new NextValue(58, Symbol()); // chain will be called for each iterated sub-shrink of root (->10)
+      const choiceShrink2Chained = new NextValue(57, Symbol()); // ->310 - 11 and 15 will not be retrieved (getNthOrLast(0))
+      generateChained
+        .mockReturnValueOnce(choiceChained)
+        .mockReturnValueOnce(choiceShrink1Chained)
+        .mockReturnValueOnce(choiceShrink2Chained);
+      const shrinkChained1 = new NextValue(25, Symbol());
+      const shrinkChained2 = new NextValue(51, Symbol());
+      shrinkChained.mockReturnValueOnce(Stream.of(shrinkChained1, shrinkChained2));
+      const shrinkChained11 = new NextValue(125, Symbol());
+      const shrinkChained12 = new NextValue(151, Symbol());
+      shrinkChained.mockReturnValueOnce(Stream.of(shrinkChained11, shrinkChained12));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+      class MyNextChainedArbitrary extends NextArbitrary<any> {
+        generate = generateChained;
+        canGenerate = canGenerateChained;
+        shrink = shrinkChained;
+      }
+      const chainer = jest.fn();
+      chainer.mockReturnValue(new MyNextChainedArbitrary());
+
+      // Act
+      const arb = new MyNextArbitrary().chain(chainer);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const firstShrunkValue = arb.shrink(g.value, g.context).getNthOrLast(0)!;
+      const shrinks = arb.shrink(firstShrunkValue.value, firstShrunkValue.context);
+
+      // Assert
+      expect([...shrinks].map((v) => v.value)).toEqual([
+        choiceShrink2Chained.value,
+        shrinkChained11.value,
+        shrinkChained12.value,
+      ]);
+      expect(generateChained).toHaveBeenNthCalledWith(2, expect.any(Random), expectedBiasFactor); // sub-sequent calls re-use the original bias
+      expect(chainer).toHaveBeenCalledWith(choiceRoot.value); // original call
+      expect(chainer).toHaveBeenCalledWith(shrinkRoot1.value); // chained during shrink
+      expect(chainer).not.toHaveBeenCalledWith(shrinkRoot2.value); // chained during shrink (skipped due to getNthOrLast(0)
+      expect(chainer).not.toHaveBeenCalledWith(shrinkRoot3.value); // chained during shrink (skipped due to getNthOrLast(0)
+      expect(chainer).toHaveBeenCalledWith(shrinkRoot11.value); // chained during second shrink (returned choiceShrink2Chained)
+    });
+
+    it('should stop shrink on source if it exhausted it once', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choiceRoot = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choiceRoot);
+      const shrinkRoot1 = new NextValue(10, Symbol());
+      const shrinkRoot2 = new NextValue(11, Symbol());
+      shrink.mockReturnValueOnce(Stream.of(shrinkRoot1, shrinkRoot2));
+      const generateChained = jest.fn();
+      const canGenerateChained = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrinkChained = jest.fn();
+      const choiceChained = new NextValue(50, Symbol());
+      const choiceShrink1Chained = new NextValue(58, Symbol());
+      const choiceShrink2Chained = new NextValue(57, Symbol());
+      generateChained
+        .mockReturnValueOnce(choiceChained)
+        .mockReturnValueOnce(choiceShrink1Chained)
+        .mockReturnValueOnce(choiceShrink2Chained);
+      const shrinkChained1 = new NextValue(25, Symbol());
+      const shrinkChained2 = new NextValue(51, Symbol());
+      shrinkChained.mockReturnValueOnce(Stream.of(shrinkChained1, shrinkChained2));
+      const shrinkChained11 = new NextValue(125, Symbol());
+      const shrinkChained12 = new NextValue(151, Symbol());
+      shrinkChained.mockReturnValueOnce(Stream.of(shrinkChained11, shrinkChained12));
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+      class MyNextChainedArbitrary extends NextArbitrary<any> {
+        generate = generateChained;
+        canGenerate = canGenerateChained;
+        shrink = shrinkChained;
+      }
+      const chainer = jest.fn();
+      chainer.mockReturnValue(new MyNextChainedArbitrary());
+
+      // Act
+      const arb = new MyNextArbitrary().chain(chainer);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrunkValue = arb.shrink(g.value, g.context).getNthOrLast(2)!; // source will be exhausted it only declares two shrunk values
+      const shrinks = arb.shrink(shrunkValue.value, shrunkValue.context);
+
+      // Assert
+      expect([...shrinks].map((v) => v.value)).toEqual([shrinkChained11.value, shrinkChained12.value]);
+      expect(shrink).toHaveBeenCalledTimes(1); // not called back on second call to shrink
+    });
+
+    it('should always return false for canGenerate when not provided any unchain function', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (value: unknown) => value is any;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().chain(() => new MyNextArbitrary());
+      const out = arb.canGenerate('');
+
+      // Assert
+      expect(out).toBe(false);
+      expect(canGenerate).not.toHaveBeenCalled();
+    });
+
+    it('should return empty stream when shrinking without any context and not provided any unchainer function', () => {
+      // Arrange
+      const generate = jest.fn();
+      const canGenerate = jest.fn();
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = (canGenerate as any) as (value: unknown) => value is any;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().chain(() => new MyNextArbitrary());
+      const shrinks = arb.shrink('');
+
+      // Assert
+      expect([...shrinks]).toHaveLength(0);
+      expect(shrink).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('noShrink', () => {
+    it('should simply return the original instance of NextValue on generate', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const generate = jest.fn();
+      const canGenerate = (jest.fn() as any) as (value: unknown) => value is any;
+      const shrink = jest.fn();
+      const choice = new NextValue(1, Symbol());
+      generate.mockReturnValueOnce(choice);
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate = canGenerate;
+        shrink = shrink;
+      }
+
+      // Act
+      const arb = new MyNextArbitrary().noShrink();
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g).toBe(choice); // just returning the instance of the source arbitrary (including its context)
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+    });
+
+    it('should override default shrink with function returning an empty Stream', () => {
+      // Arrange
+      const shrink = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate(): NextValue<any> {
+          throw new Error('Not implemented.');
+        }
+        canGenerate(value: unknown): value is any {
+          throw new Error('Not implemented.');
+        }
+        shrink = shrink;
+      }
+      const fakeArbitrary: NextArbitrary<any> = new MyNextArbitrary();
+      const noShrinkArbitrary = fakeArbitrary.noShrink();
+
+      // Act
+      const out = noShrinkArbitrary.shrink(5, Symbol());
+
+      // Assert
+      expect([...out]).toHaveLength(0);
+      expect(shrink).not.toHaveBeenCalled();
+    });
+
+    it('should return itself when called twice', () => {
+      // Arrange
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate(): NextValue<any> {
+          throw new Error('Not implemented.');
+        }
+        canGenerate(value: unknown): value is any {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<NextValue<any>> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: NextArbitrary<any> = new MyNextArbitrary();
+
+      // Act
+      const firstNoShrink = fakeArbitrary.noShrink();
+      const secondNoShrink = firstNoShrink.noShrink();
+
+      // Assert
+      expect(secondNoShrink).toBe(firstNoShrink);
+    });
+  });
+
+  describe('noBias', () => {
+    it('should override passed bias with undefined', () => {
+      // Arrange
+      const generate = jest.fn();
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate = generate;
+        canGenerate(value: unknown): value is any {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<NextValue<any>> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: NextArbitrary<any> = new MyNextArbitrary();
+      const noBiasArbitrary = fakeArbitrary.noBias();
+
+      // Act
+      noBiasArbitrary.generate(mrngNoCall, 42);
+
+      // Assert
+      expect(generate).toHaveBeenCalledTimes(1);
+      expect(generate).toHaveBeenCalledWith(mrngNoCall, undefined);
+    });
+
+    it('should return itself when called twice', () => {
+      // Arrange
+      class MyNextArbitrary extends NextArbitrary<any> {
+        generate(): NextValue<any> {
+          throw new Error('Not implemented.');
+        }
+        canGenerate(value: unknown): value is any {
+          throw new Error('Not implemented.');
+        }
+        shrink(): Stream<NextValue<any>> {
+          throw new Error('Not implemented.');
+        }
+      }
+      const fakeArbitrary: NextArbitrary<any> = new MyNextArbitrary();
+
+      // Act
+      const firstNoBias = fakeArbitrary.noBias();
+      const secondNoBias = firstNoBias.noBias();
+
+      // Assert
+      expect(secondNoBias).toBe(firstNoBias);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/definition/NextValue.utest.spec.ts b/test/unit/check/arbitrary/definition/NextValue.utest.spec.ts
new file mode 100644
index 00000000..9d40abff
--- /dev/null
+++ b/test/unit/check/arbitrary/definition/NextValue.utest.spec.ts
@@ -0,0 +1,52 @@
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { cloneMethod } from '../../../../../src/check/symbols';
+
+describe('NextValue', () => {
+  describe('cloneable values', () => {
+    it('should not clone cloneable values on first access', () => {
+      // Arrange
+      const clone = jest.fn();
+      const instance = { [cloneMethod]: clone };
+
+      // Act
+      const nextValue = new NextValue(instance);
+      const value = nextValue.value;
+
+      // Assert
+      expect(value).toBe(instance);
+      expect(clone).not.toHaveBeenCalled();
+    });
+
+    it('should clone cloneable values on second access', () => {
+      // Arrange
+      const clone = jest.fn();
+      const instance = { [cloneMethod]: clone };
+
+      // Act
+      const nextValue = new NextValue(instance);
+      const value = nextValue.value;
+      const value2 = nextValue.value;
+
+      // Assert
+      expect(value2).not.toBe(value);
+      expect(value2).not.toBe(instance);
+      expect(clone).toHaveBeenCalledTimes(1);
+    });
+
+    it('should not call cloneMethod of cloneable values on second access if customGetValue was provided', () => {
+      // Arrange
+      const clone = jest.fn();
+      const cloneOverride = jest.fn();
+      const instance = { [cloneMethod]: clone };
+
+      // Act
+      const nextValue = new NextValue(instance, null, cloneOverride);
+      nextValue.value;
+      nextValue.value;
+
+      // Assert
+      expect(clone).not.toHaveBeenCalled();
+      expect(cloneOverride).toHaveBeenCalledTimes(2);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/generic/ShrinkTree.ts b/test/unit/check/arbitrary/generic/ShrinkTree.ts
index e11cdf48..ea6382aa 100644
--- a/test/unit/check/arbitrary/generic/ShrinkTree.ts
+++ b/test/unit/check/arbitrary/generic/ShrinkTree.ts
@@ -1,10 +1,16 @@
 import * as fc from '../../../../../lib/fast-check';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
 import { Shrinkable } from '../../../../../src/check/arbitrary/definition/Shrinkable';
 
 export type ShrinkTree<T> = [T, ShrinkTree<T>[]];
 
 export function buildShrinkTree<T>(s: Shrinkable<T>): ShrinkTree<T> {
-  return [s.value_, [...s.shrink().map((ss) => buildShrinkTree(ss))]];
+  return [s.value, [...s.shrink().map((ss) => buildShrinkTree(ss))]];
+}
+
+export function buildNextShrinkTree<T>(arb: NextArbitrary<T>, v: NextValue<T>): ShrinkTree<T> {
+  return [v.value, [...arb.shrink(v.value_, v.context).map((nv) => buildNextShrinkTree(arb, nv))]];
 }
 
 export function renderTree<T>(tree: ShrinkTree<T>): string[] {
@@ -26,3 +32,10 @@ export function renderTree<T>(tree: ShrinkTree<T>): string[] {
   }
   return lines;
 }
+
+export function walkTree<T>(tree: ShrinkTree<T>, visit: (t: T) => void): void {
+  visit(tree[0]);
+  for (const subTree of tree[1]) {
+    walkTree(subTree, visit);
+  }
+}