ts-data-forge 6.13.0 → 6.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- import { type MergeIntersection } from 'ts-type-forge';
2
1
  import { Optional, type UnknownOptional } from './optional/index.mjs';
3
2
  /**
4
3
  * Creates a new pipe object that allows for chaining operations on a value.
@@ -18,15 +17,8 @@ import { Optional, type UnknownOptional } from './optional/index.mjs';
18
17
  * @param a The initial value to wrap in a pipe.
19
18
  * @returns A pipe object with chaining methods appropriate for the value type.
20
19
  */
21
- export declare function pipe<const A extends UnknownOptional>(a: A): PipeWithMapOptional<A>;
22
- export declare function pipe<const A>(a: A): PipeBase<A>;
23
- type Pipe<A> = A extends UnknownOptional ? PipeWithMapOptional<A> : PipeBase<A>;
24
- /**
25
- * @template A The type of the current value in the pipe.
26
- * @internal
27
- * Base pipe interface providing core functionality.
28
- * All pipe types extend this interface.
29
- */
20
+ export declare const pipe: <const A>(a: A) => Pipe<A>;
21
+ export type Pipe<A> = PipeBase<A> & PipeMapNullable<A> & ([A] extends [UnknownOptional] ? PipeMapOptional<A> : unknown);
30
22
  type PipeBase<A> = Readonly<{
31
23
  /** The current value being piped. */
32
24
  value: A;
@@ -48,6 +40,8 @@ type PipeBase<A> = Readonly<{
48
40
  * @returns A new pipe containing the transformed value.
49
41
  */
50
42
  map: <B>(fn: (a: A) => B) => Pipe<B>;
43
+ }>;
44
+ type PipeMapNullable<A> = Readonly<{
51
45
  /**
52
46
  * Maps the current value only if it's not null or undefined. If the current
53
47
  * value is null/undefined, the transformation is skipped and undefined is
@@ -75,13 +69,7 @@ type PipeBase<A> = Readonly<{
75
69
  */
76
70
  mapNullable: <B>(fn: (a: NonNullable<A>) => B) => Pipe<B | undefined>;
77
71
  }>;
78
- /**
79
- * @template A The Optional type currently in the pipe.
80
- * @internal
81
- * Pipe interface for Optional values, providing Optional-aware mapping.
82
- * Extends PipeBase with mapOptional functionality for monadic operations.
83
- */
84
- type PipeWithMapOptional<A extends UnknownOptional> = MergeIntersection<PipeBase<A> & Readonly<{
72
+ type PipeMapOptional<A extends UnknownOptional> = Readonly<{
85
73
  /**
86
74
  * Maps the value inside an Optional using Optional.map semantics. If the
87
75
  * Optional is None, the transformation is skipped and None is propagated.
@@ -109,6 +97,6 @@ type PipeWithMapOptional<A extends UnknownOptional> = MergeIntersection<PipeBase
109
97
  * @returns A new pipe containing an Optional with the transformed value.
110
98
  */
111
99
  mapOptional: <B>(fn: (a: Optional.Unwrap<A>) => B) => Pipe<Optional<B>>;
112
- }>>;
100
+ }>;
113
101
  export {};
114
102
  //# sourceMappingURL=pipe.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipe.d.mts","sourceRoot":"","sources":["../../src/functional/pipe.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,eAAe,EAClD,CAAC,EAAE,CAAC,GACH,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE1B,wBAAgB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AAkBjD,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,eAAe,GAAG,mBAAmB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,KAAK,QAAQ,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC1B,qCAAqC;IACrC,KAAK,EAAE,CAAC,CAAC;IAET;;;;;;;;;;;;;;;;OAgBG;IACH,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;IAErC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;CACvE,CAAC,CAAC;AAEH;;;;;GAKG;AACH,KAAK,mBAAmB,CAAC,CAAC,SAAS,eAAe,IAAI,iBAAiB,CACrE,QAAQ,CAAC,CAAC,CAAC,GACT,QAAQ,CAAC;IACP;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,CACL,CAAC"}
1
+ {"version":3,"file":"pipe.d.mts","sourceRoot":"","sources":["../../src/functional/pipe.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEtE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,IAAI,GAAI,KAAK,CAAC,CAAC,EAAG,GAAG,CAAC,KAAG,IAAI,CAAC,CAAC,CAYI,CAAC;AAmBjD,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,GAC/B,eAAe,CAAC,CAAC,CAAC,GAClB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;AAgCjE,KAAK,QAAQ,CAAC,CAAC,IAAI,QAAQ,CAAC;IAC1B,qCAAqC;IACrC,KAAK,EAAE,CAAC,CAAC;IAET;;;;;;;;;;;;;;;;OAgBG;IACH,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC,CAAC;AAEH,KAAK,eAAe,CAAC,CAAC,IAAI,QAAQ,CAAC;IACjC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;CACvE,CAAC,CAAC;AAEH,KAAK,eAAe,CAAC,CAAC,SAAS,eAAe,IAAI,QAAQ,CAAC;IACzD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC,CAAC"}
@@ -1,22 +1,42 @@
1
1
  import { isOptional } from './optional/impl/optional-is-optional.mjs';
2
2
  import { map } from './optional/impl/optional-map.mjs';
3
3
 
4
- function pipe(a) {
5
- if (isOptional(a)) {
6
- return {
7
- value: a,
8
- map: (fn) => pipe(fn(a)),
4
+ /**
5
+ * Creates a new pipe object that allows for chaining operations on a value.
6
+ *
7
+ * This function provides a fluent interface for applying transformations to
8
+ * values, with intelligent method selection based on the input type:
9
+ *
10
+ * - For `Optional` values: Provides `mapOptional` for safe Optional
11
+ * transformations
12
+ * - For other values: Provides `mapNullable` for null-safe transformations
13
+ * - All types get the basic `map` method for general transformations
14
+ *
15
+ * The pipe maintains type safety throughout the chain, automatically selecting
16
+ * the appropriate overload based on the current value type.
17
+ *
18
+ * @template A The type of the initial value to wrap in a pipe.
19
+ * @param a The initial value to wrap in a pipe.
20
+ * @returns A pipe object with chaining methods appropriate for the value type.
21
+ */
22
+ const pipe = (a) =>
23
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
24
+ ({
25
+ value: a,
26
+ map: (fn) => pipe(fn(a)),
27
+ mapNullable: (fn) => pipe(a == null ? undefined : fn(a)),
28
+ ...(isOptional(a)
29
+ ? {
9
30
  mapOptional: (fn) => pipe(map(a, fn)),
10
- };
11
- }
12
- else {
13
- return {
14
- value: a,
15
- map: (fn) => pipe(fn(a)),
16
- mapNullable: (fn) => pipe(a == null ? undefined : fn(a)),
17
- };
18
- }
19
- }
31
+ }
32
+ : {}),
33
+ });
34
+ // NOTE: 以下では 'typing when used with generics' test で型エラーが出る。
35
+ // 詳細は documents/reports/pipe-typing.md の 「mapNullable を条件付きで含めるのがうまくいかない理由」を参照。
36
+ // export type Pipe<A> = PipeBase<A> &
37
+ // ([A] extends [UnknownOptional] ? PipeMapOptional<A> : unknown) &
38
+ // ([undefined] extends [A] ? PipeMapNullable<A> : unknown) &
39
+ // ([null] extends [A] ? PipeMapNullable<A> : unknown);
20
40
 
21
41
  export { pipe };
22
42
  //# sourceMappingURL=pipe.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipe.mjs","sources":["../../src/functional/pipe.mts"],"sourcesContent":[null],"names":["Optional.isOptional","Optional.map"],"mappings":";;;AA2BM,SAAU,IAAI,CAAU,CAAI,EAAA;AAChC,IAAA,IAAIA,UAAmB,CAAC,CAAC,CAAC,EAAE;QAC1B,OAAO;AACL,YAAA,KAAK,EAAE,CAAC;AACR,YAAA,GAAG,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACxB,YAAA,WAAW,EAAE,CAAC,EAAE,KAAK,IAAI,CAACC,GAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;SAC/C;IACH;SAAO;QACL,OAAO;AACL,YAAA,KAAK,EAAE,CAAC;AACR,YAAA,GAAG,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxB,WAAW,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,IAAI,IAAI,GAAG,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;SACzD;IACH;AACF;;;;"}
1
+ {"version":3,"file":"pipe.mjs","sources":["../../src/functional/pipe.mts"],"sourcesContent":[null],"names":["Optional.isOptional","Optional.map"],"mappings":";;;AAGA;;;;;;;;;;;;;;;;;AAiBG;AACI,MAAM,IAAI,GAAG,CAAW,CAAI;AACjC;AACA,CAAC;AACC,IAAA,KAAK,EAAE,CAAC;AACR,IAAA,GAAG,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxB,WAAW,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,IAAI,IAAI,GAAG,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAExD,IAAA,IAAIA,UAAmB,CAAC,CAAC;AACvB,UAAE;AACE,YAAA,WAAW,EAAE,CAAC,EAAE,KAAK,IAAI,CAACC,GAAY,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/C;UACD,EAAE,CAAC;AACR,CAAA;AAuBH;AACA;AACA;AACA;AACA;AACA;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-data-forge",
3
- "version": "6.13.0",
3
+ "version": "6.13.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -42,6 +42,9 @@
42
42
  "clean": "pnpm run /clean:.*/",
43
43
  "clean:logs": "npx rimraf ./*.log",
44
44
  "codemod": "run-s codemod:uncommitted:as-const fmt codemod:uncommitted:readonly fmt",
45
+ "codemod:diff": "run-s codemod:diff:as-const fmt codemod:diff:readonly fmt",
46
+ "codemod:diff:as-const": "append-as-const --diff-from origin/main '{src,scripts,samples,test}/**/*.{mts,tsx}'",
47
+ "codemod:diff:readonly": "convert-to-readonly --diff-from origin/main '{src,scripts,samples,test}/**/*.{mts,tsx}'",
45
48
  "codemod:full": "run-s codemod:full:as-const fmt codemod:full:readonly fmt",
46
49
  "codemod:full:as-const": "append-as-const '{src,scripts,samples,test}/**/*.{mts,tsx}'",
47
50
  "codemod:full:readonly": "convert-to-readonly '{src,scripts,samples,test}/**/*.{mts,tsx}'",
@@ -1,4 +1,4 @@
1
- import { type MergeIntersection } from 'ts-type-forge';
1
+ import { expectType } from '../expect-type.mjs';
2
2
  import { Optional, type UnknownOptional } from './optional/index.mjs';
3
3
 
4
4
  /**
@@ -19,36 +19,71 @@ import { Optional, type UnknownOptional } from './optional/index.mjs';
19
19
  * @param a The initial value to wrap in a pipe.
20
20
  * @returns A pipe object with chaining methods appropriate for the value type.
21
21
  */
22
- export function pipe<const A extends UnknownOptional>(
23
- a: A,
24
- ): PipeWithMapOptional<A>;
25
-
26
- export function pipe<const A>(a: A): PipeBase<A>;
27
-
28
- export function pipe<const A>(a: A): PipeImpl<A> {
29
- if (Optional.isOptional(a)) {
30
- return {
31
- value: a,
32
- map: (fn) => pipe(fn(a)),
33
- mapOptional: (fn) => pipe(Optional.map(a, fn)),
34
- };
35
- } else {
36
- return {
37
- value: a,
38
- map: (fn) => pipe(fn(a)),
39
- mapNullable: (fn) => pipe(a == null ? undefined : fn(a)),
40
- };
41
- }
42
- }
43
-
44
- type Pipe<A> = A extends UnknownOptional ? PipeWithMapOptional<A> : PipeBase<A>;
22
+ export const pipe = <const A,>(a: A): Pipe<A> =>
23
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
24
+ ({
25
+ value: a,
26
+ map: (fn) => pipe(fn(a)),
27
+ mapNullable: (fn) => pipe(a == null ? undefined : fn(a)),
28
+
29
+ ...(Optional.isOptional(a)
30
+ ? {
31
+ mapOptional: (fn) => pipe(Optional.map(a, fn)),
32
+ }
33
+ : {}),
34
+ }) satisfies PipeImpl<A> as unknown as Pipe<A>;
35
+
36
+ // `PipeBase<A>` is always present; the conditional only *adds* `mapOptional`
37
+ // for Optional values. Two subtleties motivate this shape:
38
+ //
39
+ // 1. The check is `[A] extends [UnknownOptional]` (a 1-tuple), not the bare
40
+ // `A extends UnknownOptional`. A bare check distributes over unions, so a
41
+ // piped union value such as `Result<D, E> | undefined` would expand into
42
+ // `Pipe<Ok<D>> | Pipe<Err<E>> | Pipe<undefined>`. Calling a chained method
43
+ // on that union of pipe objects forces TypeScript to synthesize a merged
44
+ // call signature, during which a branded member like `Ok<D>` is widened to
45
+ // its constraint (`Ok<number>`) — losing the brand `D`.
46
+ //
47
+ // 2. `value: A` lives on `PipeBase<A>`, *outside* the conditional. If `value`
48
+ // flowed through the conditional, then for a not-yet-resolved generic `A`
49
+ // the deferred true branch narrows `A` to `A & UnknownOptional`, so
50
+ // `.value` would surface as the noisy `(A & UnknownOptional) | A`. Keeping
51
+ // it on the unconditional base yields a clean `A`.
52
+ // transformer-ignore-next-line convert-to-readonly
53
+ export type Pipe<A> = PipeBase<A> &
54
+ PipeMapNullable<A> &
55
+ ([A] extends [UnknownOptional] ? PipeMapOptional<A> : unknown);
56
+
57
+ // NOTE: 以下では 'typing when used with generics' test で型エラーが出る。
58
+ // 詳細は documents/reports/pipe-typing.md の 「mapNullable を条件付きで含めるのがうまくいかない理由」を参照。
59
+ // export type Pipe<A> = PipeBase<A> &
60
+ // ([A] extends [UnknownOptional] ? PipeMapOptional<A> : unknown) &
61
+ // ([undefined] extends [A] ? PipeMapNullable<A> : unknown) &
62
+ // ([null] extends [A] ? PipeMapNullable<A> : unknown);
63
+
64
+ expectType<Optional<number>, UnknownOptional>('<=');
65
+
66
+ expectType<number | undefined, undefined>('>=');
67
+
68
+ expectType<keyof Pipe<number | undefined>, 'value' | 'map' | 'mapNullable'>(
69
+ '=',
70
+ );
71
+
72
+ expectType<
73
+ keyof Pipe<Optional<number>>,
74
+ 'value' | 'map' | 'mapNullable' | 'mapOptional'
75
+ >('=');
76
+
77
+ expectType<
78
+ keyof Pipe<Optional<number | undefined>>,
79
+ 'value' | 'map' | 'mapNullable' | 'mapOptional'
80
+ >('=');
81
+
82
+ expectType<
83
+ keyof Pipe<Optional<number> | undefined>,
84
+ 'value' | 'map' | 'mapNullable'
85
+ >('=');
45
86
 
46
- /**
47
- * @template A The type of the current value in the pipe.
48
- * @internal
49
- * Base pipe interface providing core functionality.
50
- * All pipe types extend this interface.
51
- */
52
87
  type PipeBase<A> = Readonly<{
53
88
  /** The current value being piped. */
54
89
  value: A;
@@ -71,7 +106,9 @@ type PipeBase<A> = Readonly<{
71
106
  * @returns A new pipe containing the transformed value.
72
107
  */
73
108
  map: <B>(fn: (a: A) => B) => Pipe<B>;
109
+ }>;
74
110
 
111
+ type PipeMapNullable<A> = Readonly<{
75
112
  /**
76
113
  * Maps the current value only if it's not null or undefined. If the current
77
114
  * value is null/undefined, the transformation is skipped and undefined is
@@ -100,56 +137,45 @@ type PipeBase<A> = Readonly<{
100
137
  mapNullable: <B>(fn: (a: NonNullable<A>) => B) => Pipe<B | undefined>;
101
138
  }>;
102
139
 
103
- /**
104
- * @template A The Optional type currently in the pipe.
105
- * @internal
106
- * Pipe interface for Optional values, providing Optional-aware mapping.
107
- * Extends PipeBase with mapOptional functionality for monadic operations.
108
- */
109
- type PipeWithMapOptional<A extends UnknownOptional> = MergeIntersection<
110
- PipeBase<A> &
111
- Readonly<{
112
- /**
113
- * Maps the value inside an Optional using Optional.map semantics. If the
114
- * Optional is None, the transformation is skipped and None is propagated.
115
- * If the Optional is Some, the transformation is applied to the inner
116
- * value.
117
- *
118
- * @example
119
- *
120
- * ```ts
121
- * const result = pipe(Optional.some(10))
122
- * .mapOptional((value) => value * 2)
123
- * .mapOptional((value) => value + 5);
124
- *
125
- * assert.deepStrictEqual(result.value, Optional.some(25));
126
- *
127
- * const empty = pipe(Optional.none as Optional<number>).mapOptional(
128
- * (value) => value * 2,
129
- * );
130
- *
131
- * assert.isTrue(Optional.isNone(empty.value));
132
- * ```
133
- *
134
- * @template B The type of the transformed inner value.
135
- * @param fn Function to transform the inner value of the Optional.
136
- * @returns A new pipe containing an Optional with the transformed value.
137
- */
138
- mapOptional: <B>(fn: (a: Optional.Unwrap<A>) => B) => Pipe<Optional<B>>;
139
- }>
140
- >;
140
+ type PipeMapOptional<A extends UnknownOptional> = Readonly<{
141
+ /**
142
+ * Maps the value inside an Optional using Optional.map semantics. If the
143
+ * Optional is None, the transformation is skipped and None is propagated.
144
+ * If the Optional is Some, the transformation is applied to the inner
145
+ * value.
146
+ *
147
+ * @example
148
+ *
149
+ * ```ts
150
+ * const result = pipe(Optional.some(10))
151
+ * .mapOptional((value) => value * 2)
152
+ * .mapOptional((value) => value + 5);
153
+ *
154
+ * assert.deepStrictEqual(result.value, Optional.some(25));
155
+ *
156
+ * const empty = pipe(Optional.none as Optional<number>).mapOptional(
157
+ * (value) => value * 2,
158
+ * );
159
+ *
160
+ * assert.isTrue(Optional.isNone(empty.value));
161
+ * ```
162
+ *
163
+ * @template B The type of the transformed inner value.
164
+ * @param fn Function to transform the inner value of the Optional.
165
+ * @returns A new pipe containing an Optional with the transformed value.
166
+ */
167
+ mapOptional: <B>(fn: (a: Optional.Unwrap<A>) => B) => Pipe<Optional<B>>;
168
+ }>;
141
169
 
142
170
  /** @internal */
143
171
  type Cast<T, U> = T & U;
144
172
 
145
173
  /** @internal */
146
- type PipeImpl<A> = Partial<
147
- Readonly<{
148
- value: A;
149
- map: <B>(fn: (a: A) => B) => PipeBase<B>;
150
- mapNullable: <B>(fn: (a: NonNullable<A>) => B) => PipeBase<B | undefined>;
151
- mapOptional: <B>(
152
- fn: (a: Optional.Unwrap<Cast<A, UnknownOptional>>) => B,
153
- ) => PipeBase<Optional<B>>;
154
- }>
155
- >;
174
+ type PipeImpl<A> = Readonly<{
175
+ value: A;
176
+ map: <B>(fn: (a: A) => B) => PipeImpl<B>;
177
+ mapNullable?: <B>(fn: (a: NonNullable<A>) => B) => PipeImpl<B | undefined>;
178
+ mapOptional?: <B>(
179
+ fn: (a: Optional.Unwrap<Cast<A, UnknownOptional>>) => B,
180
+ ) => PipeImpl<Optional<B>>;
181
+ }>;
@@ -2,6 +2,7 @@ import { expectType } from '../expect-type.mjs';
2
2
  import { type None, type Some } from '../types.mjs';
3
3
  import { Optional } from './optional/index.mjs';
4
4
  import { pipe } from './pipe.mjs';
5
+ import { Result } from './result/index.mjs';
5
6
 
6
7
  describe(pipe, () => {
7
8
  test('basic pipe operations', () => {
@@ -35,7 +36,9 @@ describe(pipe, () => {
35
36
  });
36
37
 
37
38
  test('mapNullable with non-null value', () => {
38
- const result = pipe(5 as number | null).mapNullable((x) => x * 2).value;
39
+ const s = 5 as number | null;
40
+
41
+ const result = pipe(s).mapNullable((x) => x * 2).value;
39
42
 
40
43
  expect(result).toBe(10);
41
44
 
@@ -95,4 +98,57 @@ describe(pipe, () => {
95
98
 
96
99
  expectType<typeof result, string>('=');
97
100
  });
101
+
102
+ test('typing when used with Result', () => {
103
+ const validate = <D,>(_u: unknown): Result<D, unknown> =>
104
+ Result.err(undefined);
105
+
106
+ const decodeBranded = (
107
+ input: string,
108
+ parse: (s: string) => number | undefined,
109
+ ): number | undefined => {
110
+ const v = pipe(input)
111
+ .map(parse)
112
+ .mapNullable(validate<number>)
113
+ .mapNullable((res) => Result.unwrapOk(res)).value;
114
+
115
+ expectType<typeof v, number | undefined>('=');
116
+
117
+ return v;
118
+ };
119
+
120
+ expect(decodeBranded('', () => undefined)).toBeUndefined();
121
+ });
122
+
123
+ test('typing when used with generics', () => {
124
+ const validate = <D,>(_u: unknown): Result<D, unknown> =>
125
+ Result.err(undefined);
126
+
127
+ // Regression: a generic brand `D` must survive the whole chain.
128
+ //
129
+ // The trigger is a `.map` whose result is a branded *union* such as
130
+ // `D | undefined`. If `Pipe<A>` distributes over unions, that step expands
131
+ // into `Pipe<D> | Pipe<undefined>`; calling the next method on the union of
132
+ // pipe objects then widens `D` to its constraint `number`, so `.value`
133
+ // collapses to `number | undefined` instead of `D | undefined`.
134
+ //
135
+ // The explicit `D | undefined` return annotation is itself the assertion:
136
+ // were the brand lost, `.value` would be `number | undefined` and the
137
+ // body would fail to compile.
138
+ const decodeBranded = <D extends number>(
139
+ input: string,
140
+ parse: (s: string) => D | undefined,
141
+ ): D | undefined => {
142
+ const v = pipe(input)
143
+ .map(parse)
144
+ .mapNullable((n) => validate<D>(n))
145
+ .mapNullable((res) => Result.unwrapOk(res)).value;
146
+
147
+ expectType<typeof v, D | undefined>('=');
148
+
149
+ return v;
150
+ };
151
+
152
+ expect(decodeBranded('', () => undefined)).toBeUndefined();
153
+ });
98
154
  });
@@ -143,10 +143,9 @@ describe(mapNullable, () => {
143
143
 
144
144
  const toUpperCase = mapNullable((s: string) => s.toUpperCase());
145
145
 
146
- const result = pipe(42 as number | null)
147
- .map(toStr)
148
- .map(addPrefix)
149
- .map(toUpperCase).value;
146
+ const x = 42 as number | null;
147
+
148
+ const result = pipe(x).map(toStr).map(addPrefix).map(toUpperCase).value;
150
149
 
151
150
  expect(result).toBe('NUMBER: 42');
152
151
  });