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.
- package/dist/functional/pipe.d.mts +6 -18
- package/dist/functional/pipe.d.mts.map +1 -1
- package/dist/functional/pipe.mjs +35 -15
- package/dist/functional/pipe.mjs.map +1 -1
- package/package.json +4 -1
- package/src/functional/pipe.mts +104 -78
- package/src/functional/pipe.test.mts +57 -1
- package/src/others/map-nullable.test.mts +3 -4
|
@@ -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
|
|
22
|
-
export
|
|
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":"
|
|
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"}
|
package/dist/functional/pipe.mjs
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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":";;;
|
|
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.
|
|
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}'",
|
package/src/functional/pipe.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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> =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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
|
});
|