ts-data-forge 6.12.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;;;;"}
@@ -1,4 +1,4 @@
1
- import { type Decrement, type Increment, type Index, type Int, type Min, type NaNType, type NegativeIndex, type NonNegativeNumber, type NonZeroNumber, type PositiveNumber, type PositiveSafeIntWithSmallInt, type RelaxedExclude, type SmallInt, type SmallUint } from 'ts-type-forge';
1
+ import { type Decrement, type FiniteNumber, type Increment, type Index, type Int, type Min, type NaNType, type NegativeIndex, type NonNegativeNumber, type NonZeroNumber, type PositiveNumber, type PositiveSafeIntWithSmallInt, type RelaxedExclude, type SmallInt, type SmallUint } from 'ts-type-forge';
2
2
  import { Result } from '../functional/index.mjs';
3
3
  import { type SmallPositiveInt } from '../types.mjs';
4
4
  /**
@@ -98,6 +98,70 @@ export declare namespace Num {
98
98
  * wrapping an `Error` describing the invalid input.
99
99
  */
100
100
  export const safeParseInt: (s: string) => Result<Int, Error>;
101
+ /**
102
+ * Safely parses a finite floating-point number from a string, returning a
103
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
104
+ * otherwise.
105
+ *
106
+ * This is a stricter alternative to both `parseFloat` and `Number`:
107
+ *
108
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
109
+ * characters make the whole input invalid and yield `Err`.
110
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
111
+ * whitespace-only input yields `Err`.
112
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
113
+ * yield `Err`.
114
+ *
115
+ * The empty-string case is rejected by delegating to `parseFloat` (which
116
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
117
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
118
+ * so `'12.9'` stays `12.9`.
119
+ *
120
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
121
+ * `Result.unwrapOkOr` to get a plain number back.
122
+ *
123
+ * @example
124
+ *
125
+ * ```ts
126
+ * assert.strictEqual(
127
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
128
+ * 12.9,
129
+ * );
130
+ *
131
+ * assert.strictEqual(
132
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
133
+ * -3.5,
134
+ * );
135
+ *
136
+ * assert.strictEqual(
137
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
138
+ * 1000,
139
+ * );
140
+ *
141
+ * // Native `parseFloat` ignores trailing non-numeric characters
142
+ *
143
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
144
+ *
145
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
146
+ *
147
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
148
+ *
149
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
150
+ *
151
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
152
+ *
153
+ * // Infinity and NaN are not finite, so they are rejected.
154
+ *
155
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
156
+ *
157
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
158
+ * ```
159
+ *
160
+ * @param s The string to parse.
161
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
162
+ * `Result.err` wrapping an `Error` describing the invalid input.
163
+ */
164
+ export const safeParseFloat: (s: string) => Result<FiniteNumber, Error>;
101
165
  /**
102
166
  * Type guard that checks if a number is non-zero.
103
167
  *
@@ -1 +1 @@
1
- {"version":3,"file":"num.d.mts","sourceRoot":"","sources":["../../src/number/num.mts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"num.d.mts","sourceRoot":"","sources":["../../src/number/num.mts"],"names":[],"mappings":"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"}
@@ -109,6 +109,82 @@ var Num;
109
109
  : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
110
110
  ok(Math.trunc(viaNumber));
111
111
  };
112
+ /**
113
+ * Safely parses a finite floating-point number from a string, returning a
114
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
115
+ * otherwise.
116
+ *
117
+ * This is a stricter alternative to both `parseFloat` and `Number`:
118
+ *
119
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
120
+ * characters make the whole input invalid and yield `Err`.
121
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
122
+ * whitespace-only input yields `Err`.
123
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
124
+ * yield `Err`.
125
+ *
126
+ * The empty-string case is rejected by delegating to `parseFloat` (which
127
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
128
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
129
+ * so `'12.9'` stays `12.9`.
130
+ *
131
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
132
+ * `Result.unwrapOkOr` to get a plain number back.
133
+ *
134
+ * @example
135
+ *
136
+ * ```ts
137
+ * assert.strictEqual(
138
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
139
+ * 12.9,
140
+ * );
141
+ *
142
+ * assert.strictEqual(
143
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
144
+ * -3.5,
145
+ * );
146
+ *
147
+ * assert.strictEqual(
148
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
149
+ * 1000,
150
+ * );
151
+ *
152
+ * // Native `parseFloat` ignores trailing non-numeric characters
153
+ *
154
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
155
+ *
156
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
157
+ *
158
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
159
+ *
160
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
161
+ *
162
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
163
+ *
164
+ * // Infinity and NaN are not finite, so they are rejected.
165
+ *
166
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
167
+ *
168
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
169
+ * ```
170
+ *
171
+ * @param s The string to parse.
172
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
173
+ * `Result.err` wrapping an `Error` describing the invalid input.
174
+ */
175
+ Num.safeParseFloat = (s) => {
176
+ const viaNumber = Number(s);
177
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseFloat` は NaN を返す。
178
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
179
+ // 非 NaN かつ有限の場合のみ採用することで空文字・空白のみ・末尾不正・
180
+ // Infinity をまとめて弾く。
181
+ return Number.isNaN(viaNumber) ||
182
+ !Number.isFinite(viaNumber) ||
183
+ Number.isNaN(Number.parseFloat(s))
184
+ ? err(new Error(`safeParseFloat: "${s}" is not a valid finite number`))
185
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
186
+ ok(viaNumber);
187
+ };
112
188
  /**
113
189
  * Type guard that checks if a number is non-zero.
114
190
  *
@@ -1 +1 @@
1
- {"version":3,"file":"num.mjs","sources":["../../src/number/num.mts"],"sourcesContent":[null],"names":["Result.err","Result.ok"],"mappings":"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"}
1
+ {"version":3,"file":"num.mjs","sources":["../../src/number/num.mts"],"sourcesContent":[null],"names":["Result.err","Result.ok"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-data-forge",
3
- "version": "6.12.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
  });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type Decrement,
3
+ type FiniteNumber,
3
4
  type Increment,
4
5
  type Index,
5
6
  type Int,
@@ -130,6 +131,86 @@ export namespace Num {
130
131
  Result.ok(Math.trunc(viaNumber) as Int);
131
132
  };
132
133
 
134
+ /**
135
+ * Safely parses a finite floating-point number from a string, returning a
136
+ * {@link Result} that is `Ok<FiniteNumber>` for valid input and `Err<Error>`
137
+ * otherwise.
138
+ *
139
+ * This is a stricter alternative to both `parseFloat` and `Number`:
140
+ *
141
+ * - Unlike `parseFloat('12abc')` (which returns `12`), trailing non-numeric
142
+ * characters make the whole input invalid and yield `Err`.
143
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
144
+ * whitespace-only input yields `Err`.
145
+ * - Unlike `Number('Infinity')` (which returns `Infinity`), non-finite values
146
+ * yield `Err`.
147
+ *
148
+ * The empty-string case is rejected by delegating to `parseFloat` (which
149
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
150
+ * garbage case is rejected via `Number`. Decimal values are preserved as-is,
151
+ * so `'12.9'` stays `12.9`.
152
+ *
153
+ * Use `Result.unwrapOk` (optionally with a `?? Number.NaN` fallback) or
154
+ * `Result.unwrapOkOr` to get a plain number back.
155
+ *
156
+ * @example
157
+ *
158
+ * ```ts
159
+ * assert.strictEqual(
160
+ * Result.unwrapOkOr(Num.safeParseFloat('12.9'), Number.NaN),
161
+ * 12.9,
162
+ * );
163
+ *
164
+ * assert.strictEqual(
165
+ * Result.unwrapOkOr(Num.safeParseFloat('-3.5'), Number.NaN),
166
+ * -3.5,
167
+ * );
168
+ *
169
+ * assert.strictEqual(
170
+ * Result.unwrapOkOr(Num.safeParseFloat('1e3'), Number.NaN),
171
+ * 1000,
172
+ * );
173
+ *
174
+ * // Native `parseFloat` ignores trailing non-numeric characters
175
+ *
176
+ * assert.strictEqual(Number.parseFloat('12px'), 12);
177
+ *
178
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
179
+ *
180
+ * // Whitespace is not a valid number, so we return an error instead of coercing to 0.
181
+ *
182
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('')));
183
+ *
184
+ * assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
185
+ *
186
+ * // Infinity and NaN are not finite, so they are rejected.
187
+ *
188
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
189
+ *
190
+ * assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
191
+ * ```
192
+ *
193
+ * @param s The string to parse.
194
+ * @returns `Result.ok(parsedFloat)` for valid finite input, otherwise
195
+ * `Result.err` wrapping an `Error` describing the invalid input.
196
+ */
197
+ export const safeParseFloat = (s: string): Result<FiniteNumber, Error> => {
198
+ const viaNumber = Number(s);
199
+
200
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseFloat` は NaN を返す。
201
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
202
+ // 非 NaN かつ有限の場合のみ採用することで空文字・空白のみ・末尾不正・
203
+ // Infinity をまとめて弾く。
204
+ return Number.isNaN(viaNumber) ||
205
+ !Number.isFinite(viaNumber) ||
206
+ Number.isNaN(Number.parseFloat(s))
207
+ ? Result.err(
208
+ new Error(`safeParseFloat: "${s}" is not a valid finite number`),
209
+ )
210
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
211
+ Result.ok(viaNumber as FiniteNumber);
212
+ };
213
+
133
214
  /**
134
215
  * Type guard that checks if a number is non-zero.
135
216
  *
@@ -190,6 +190,68 @@ describe('Num test', () => {
190
190
  });
191
191
  });
192
192
 
193
+ describe('safeParseFloat', () => {
194
+ test('parses valid numeric strings into Ok', () => {
195
+ expect(Result.unwrapOk(Num.safeParseFloat('123'))).toBe(123);
196
+
197
+ expect(Result.unwrapOk(Num.safeParseFloat('-42'))).toBe(-42);
198
+
199
+ expect(Result.unwrapOk(Num.safeParseFloat('+7'))).toBe(7);
200
+
201
+ expect(Result.unwrapOk(Num.safeParseFloat('0'))).toBe(0);
202
+
203
+ expect(Result.unwrapOk(Num.safeParseFloat(' 12 '))).toBe(12);
204
+ });
205
+
206
+ test('preserves decimal values', () => {
207
+ expect(Result.unwrapOk(Num.safeParseFloat('12.9'))).toBe(12.9);
208
+
209
+ expect(Result.unwrapOk(Num.safeParseFloat('-3.5'))).toBe(-3.5);
210
+
211
+ expect(Result.unwrapOk(Num.safeParseFloat('1e3'))).toBe(1000);
212
+ });
213
+
214
+ test('rejects trailing non-numeric characters (unlike parseFloat)', () => {
215
+ assert.isTrue(Result.isErr(Num.safeParseFloat('123abc')));
216
+
217
+ assert.isTrue(Result.isErr(Num.safeParseFloat('12px')));
218
+
219
+ assert.isTrue(Result.isErr(Num.safeParseFloat('abc')));
220
+ });
221
+
222
+ test('rejects empty / whitespace-only input (unlike Number)', () => {
223
+ assert.isTrue(Result.isErr(Num.safeParseFloat('')));
224
+
225
+ assert.isTrue(Result.isErr(Num.safeParseFloat(' ')));
226
+ });
227
+
228
+ test('rejects non-finite values', () => {
229
+ assert.isTrue(Result.isErr(Num.safeParseFloat('Infinity')));
230
+
231
+ assert.isTrue(Result.isErr(Num.safeParseFloat('-Infinity')));
232
+
233
+ assert.isTrue(Result.isErr(Num.safeParseFloat('NaN')));
234
+ });
235
+
236
+ test('the Err carries a descriptive Error', () => {
237
+ const result = Num.safeParseFloat('nope');
238
+
239
+ assert.isTrue(Result.isErr(result));
240
+
241
+ if (Result.isErr(result)) {
242
+ expect(Result.unwrapErr(result)).toBeInstanceOf(Error);
243
+ }
244
+ });
245
+
246
+ test('composes with Result.unwrapOk + nullish fallback', () => {
247
+ expect(Result.unwrapOk(Num.safeParseFloat('3.14')) ?? Number.NaN).toBe(
248
+ 3.14,
249
+ );
250
+
251
+ expect(Result.unwrapOk(Num.safeParseFloat('')) ?? Number.NaN).toBeNaN();
252
+ });
253
+ });
254
+
193
255
  describe('isInRange', () => {
194
256
  test('checks range (lower inclusive, upper exclusive)', () => {
195
257
  const inRange = Num.isInRange(0, 10);
@@ -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
  });