ts-data-forge 6.10.0 → 6.12.0

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,4 @@
1
- import { type RelaxedExclude } from 'ts-type-forge';
1
+ import { type NonEmptyString, type RelaxedExclude } from 'ts-type-forge';
2
2
  /**
3
3
  * Type guard that checks if a value is a non-empty string.
4
4
  *
@@ -36,5 +36,5 @@ import { type RelaxedExclude } from 'ts-type-forge';
36
36
  * `true`, TypeScript narrows the type to exclude empty strings and nullish
37
37
  * values.
38
38
  */
39
- export declare const isNonEmptyString: <S extends string | null | undefined>(s: S) => s is RelaxedExclude<NonNullable<S>, "">;
39
+ export declare const isNonEmptyString: <S extends string | null | undefined>(s: S) => s is NonEmptyString & RelaxedExclude<NonNullable<S>, "">;
40
40
  //# sourceMappingURL=is-non-empty-string.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"is-non-empty-string.d.mts","sourceRoot":"","sources":["../../src/guard/is-non-empty-string.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAClE,GAAG,CAAC,KACH,CAAC,IAAI,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CACF,CAAC"}
1
+ {"version":3,"file":"is-non-empty-string.d.mts","sourceRoot":"","sources":["../../src/guard/is-non-empty-string.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,EAClE,GAAG,CAAC,KACH,CAAC,IAAI,cAAc,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CACnB,CAAC"}
@@ -1,4 +1,5 @@
1
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';
2
+ import { Result } from '../functional/index.mjs';
2
3
  import { type SmallPositiveInt } from '../types.mjs';
3
4
  /**
4
5
  * Namespace providing utility functions for number manipulation and validation.
@@ -33,6 +34,70 @@ export declare namespace Num {
33
34
  * @returns The numeric representation of `n`.
34
35
  */
35
36
  export const from: (n: unknown) => number;
37
+ /**
38
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
39
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
40
+ *
41
+ * This is a stricter alternative to both `parseInt` and `Number`:
42
+ *
43
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
44
+ * non-numeric characters make the whole input invalid and yield `Err`.
45
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
46
+ * whitespace-only input yields `Err`.
47
+ *
48
+ * The empty-string case is rejected by delegating to `parseInt` (which
49
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
50
+ * garbage case is rejected via `Number`. Valid input is truncated toward
51
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
52
+ *
53
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
54
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
55
+ * back.
56
+ *
57
+ * @example
58
+ *
59
+ * ```ts
60
+ * assert.strictEqual(
61
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
62
+ * 123,
63
+ * );
64
+ *
65
+ * assert.strictEqual(
66
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
67
+ * 12,
68
+ * );
69
+ *
70
+ * assert.strictEqual(
71
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
72
+ * -12,
73
+ * );
74
+ *
75
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
76
+ *
77
+ * // Native `parseInt` ignores trailing non-numeric characters
78
+ *
79
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
80
+ *
81
+ * assert.isTrue(Number.isNaN(Number('123abc')));
82
+ *
83
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
84
+ *
85
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
86
+ *
87
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
88
+ *
89
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
90
+ *
91
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
92
+ *
93
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
94
+ * ```
95
+ *
96
+ * @param s The string to parse.
97
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
98
+ * wrapping an `Error` describing the invalid input.
99
+ */
100
+ export const safeParseInt: (s: string) => Result<Int, Error>;
36
101
  /**
37
102
  * Type guard that checks if a number is non-zero.
38
103
  *
@@ -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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"}
@@ -1,3 +1,7 @@
1
+ import { err } from '../functional/result/impl/result-err.mjs';
2
+ import { ok } from '../functional/result/impl/result-ok.mjs';
3
+ import '@sindresorhus/is';
4
+
1
5
  /**
2
6
  * Namespace providing utility functions for number manipulation and validation.
3
7
  *
@@ -32,6 +36,79 @@ var Num;
32
36
  * @returns The numeric representation of `n`.
33
37
  */
34
38
  Num.from = Number;
39
+ /**
40
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
41
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
42
+ *
43
+ * This is a stricter alternative to both `parseInt` and `Number`:
44
+ *
45
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
46
+ * non-numeric characters make the whole input invalid and yield `Err`.
47
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
48
+ * whitespace-only input yields `Err`.
49
+ *
50
+ * The empty-string case is rejected by delegating to `parseInt` (which
51
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
52
+ * garbage case is rejected via `Number`. Valid input is truncated toward
53
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
54
+ *
55
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
56
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
57
+ * back.
58
+ *
59
+ * @example
60
+ *
61
+ * ```ts
62
+ * assert.strictEqual(
63
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
64
+ * 123,
65
+ * );
66
+ *
67
+ * assert.strictEqual(
68
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
69
+ * 12,
70
+ * );
71
+ *
72
+ * assert.strictEqual(
73
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
74
+ * -12,
75
+ * );
76
+ *
77
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
78
+ *
79
+ * // Native `parseInt` ignores trailing non-numeric characters
80
+ *
81
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
82
+ *
83
+ * assert.isTrue(Number.isNaN(Number('123abc')));
84
+ *
85
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
86
+ *
87
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
88
+ *
89
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
90
+ *
91
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
92
+ *
93
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
94
+ *
95
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
96
+ * ```
97
+ *
98
+ * @param s The string to parse.
99
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
100
+ * wrapping an `Error` describing the invalid input.
101
+ */
102
+ Num.safeParseInt = (s) => {
103
+ const viaNumber = Number(s);
104
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseInt` は NaN を返す。
105
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
106
+ // 有効な場合のみ採用することで空文字・空白のみ・末尾不正をまとめて弾く。
107
+ return Number.isNaN(viaNumber) || Number.isNaN(Number.parseInt(s, 10))
108
+ ? err(new Error(`safeParseInt: "${s}" is not a valid base-10 integer`))
109
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
110
+ ok(Math.trunc(viaNumber));
111
+ };
35
112
  /**
36
113
  * Type guard that checks if a number is non-zero.
37
114
  *
@@ -1 +1 @@
1
- {"version":3,"file":"num.mjs","sources":["../../src/number/num.mts"],"sourcesContent":[null],"names":[],"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.10.0",
3
+ "version": "6.12.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -82,13 +82,13 @@
82
82
  "z:vitest:node": "pnpm run z:vitest --project='Node.js'"
83
83
  },
84
84
  "dependencies": {
85
- "@sindresorhus/is": "^8.0.0",
86
- "ts-type-forge": "^3.0.1"
85
+ "@sindresorhus/is": "^8.1.0",
86
+ "ts-type-forge": "^3.2.0"
87
87
  },
88
88
  "devDependencies": {
89
89
  "@emotion/react": "11.14.0",
90
90
  "@emotion/styled": "11.14.1",
91
- "@mui/material": "9.0.0",
91
+ "@mui/material": "9.1.1",
92
92
  "@rollup/plugin-replace": "6.0.3",
93
93
  "@rollup/plugin-strip": "3.0.4",
94
94
  "@rollup/plugin-typescript": "12.3.0",
@@ -96,51 +96,51 @@
96
96
  "@semantic-release/commit-analyzer": "13.0.1",
97
97
  "@semantic-release/exec": "7.1.0",
98
98
  "@semantic-release/git": "10.0.1",
99
- "@semantic-release/github": "12.0.6",
99
+ "@semantic-release/github": "12.0.8",
100
100
  "@semantic-release/npm": "13.1.5",
101
- "@semantic-release/release-notes-generator": "14.1.0",
102
- "@types/node": "25.6.0",
103
- "@types/react": "19.2.14",
104
- "@vitest/browser-playwright": "4.1.5",
105
- "@vitest/coverage-v8": "4.1.5",
106
- "@vitest/ui": "4.1.5",
101
+ "@semantic-release/release-notes-generator": "14.1.1",
102
+ "@types/node": "26.0.0",
103
+ "@types/react": "19.2.17",
104
+ "@vitest/browser-playwright": "4.1.9",
105
+ "@vitest/coverage-v8": "4.1.9",
106
+ "@vitest/ui": "4.1.9",
107
107
  "conventional-changelog-conventionalcommits": "9.3.1",
108
- "cspell": "10.0.0",
108
+ "cspell": "10.0.1",
109
109
  "dedent": "1.7.2",
110
110
  "eslint": "9.39.4",
111
- "eslint-config-typed": "4.9.8",
111
+ "eslint-config-typed": "4.9.11",
112
112
  "github-settings-as-code": "1.2.9",
113
- "immer": "11.1.6",
114
- "jiti": "2.6.1",
115
- "markdownlint": "0.40.0",
113
+ "immer": "11.1.8",
114
+ "jiti": "2.7.0",
115
+ "markdownlint": "0.41.0",
116
116
  "markdownlint-cli2": "0.22.1",
117
- "npm-run-all2": "8.0.4",
118
- "playwright": "1.59.1",
119
- "prettier": "3.8.3",
117
+ "npm-run-all2": "9.0.2",
118
+ "playwright": "1.61.0",
119
+ "prettier": "3.8.4",
120
120
  "prettier-plugin-organize-imports": "4.3.0",
121
121
  "prettier-plugin-packagejson": "3.0.2",
122
- "react": "19.2.5",
123
- "rollup": "4.60.3",
124
- "semantic-release": "25.0.3",
125
- "ts-codemod-lib": "2.1.7",
126
- "ts-repo-utils": "10.0.3",
122
+ "react": "19.2.7",
123
+ "rollup": "4.62.0",
124
+ "semantic-release": "25.0.5",
125
+ "ts-codemod-lib": "2.2.0",
126
+ "ts-repo-utils": "10.1.2",
127
127
  "tslib": "2.8.1",
128
- "tsx": "4.21.0",
128
+ "tsx": "4.22.4",
129
129
  "typedoc": "0.28.19",
130
130
  "typedoc-github-theme": "0.4.0",
131
131
  "typescript": "5.9.3",
132
132
  "vite": "8.0.16",
133
- "vitest": "4.1.5"
133
+ "vitest": "4.1.9"
134
134
  },
135
135
  "peerDependencies": {
136
136
  "typescript": ">=4.8"
137
137
  },
138
- "packageManager": "pnpm@10.33.4",
138
+ "packageManager": "pnpm@10.34.4",
139
139
  "engines": {
140
- "node": ">=22.18.0",
140
+ "node": ">=22.22.2",
141
141
  "pnpm": ">=9.0.0"
142
142
  },
143
143
  "volta": {
144
- "node": "25.9.0"
144
+ "node": "26.3.1"
145
145
  }
146
146
  }
@@ -1,4 +1,4 @@
1
- import { type RelaxedExclude } from 'ts-type-forge';
1
+ import { type NonEmptyString, type RelaxedExclude } from 'ts-type-forge';
2
2
 
3
3
  /**
4
4
  * Type guard that checks if a value is a non-empty string.
@@ -39,5 +39,5 @@ import { type RelaxedExclude } from 'ts-type-forge';
39
39
  */
40
40
  export const isNonEmptyString = <S extends string | null | undefined>(
41
41
  s: S,
42
- ): s is RelaxedExclude<NonNullable<S>, ''> =>
42
+ ): s is NonEmptyString & RelaxedExclude<NonNullable<S>, ''> =>
43
43
  typeof s === 'string' && s.length > 0;
@@ -1,3 +1,4 @@
1
+ import { type NonEmptyString } from 'ts-type-forge';
1
2
  import { expectType } from '../expect-type.mjs';
2
3
  import { isNonEmptyString } from './is-non-empty-string.mjs';
3
4
 
@@ -55,7 +56,7 @@ describe(isNonEmptyString, () => {
55
56
 
56
57
  // @ts-expect-error Testing non-string types
57
58
  if (isNonEmptyString(value)) {
58
- expectType<typeof value, string>('=');
59
+ expectType<typeof value, NonEmptyString>('=');
59
60
 
60
61
  // TypeScript knows it's a string
61
62
  expect(value.length).toBeGreaterThan(0);
@@ -68,7 +69,7 @@ describe(isNonEmptyString, () => {
68
69
  const maybeString: string | undefined | null = 'hello';
69
70
 
70
71
  if (isNonEmptyString(maybeString)) {
71
- expectType<typeof maybeString, string>('=');
72
+ expectType<typeof maybeString, NonEmptyString>('=');
72
73
 
73
74
  expect(maybeString.toUpperCase()).toBe('HELLO');
74
75
  }
@@ -16,6 +16,7 @@ import {
16
16
  type UnknownBrand,
17
17
  } from 'ts-type-forge';
18
18
  import { expectType } from '../expect-type.mjs';
19
+ import { Result } from '../functional/index.mjs';
19
20
  import { type SmallPositiveInt } from '../types.mjs';
20
21
 
21
22
  /**
@@ -52,6 +53,83 @@ export namespace Num {
52
53
  */
53
54
  export const from: (n: unknown) => number = Number;
54
55
 
56
+ /**
57
+ * Safely parses a base-10 integer from a string, returning a {@link Result}
58
+ * that is `Ok<Int>` for valid input and `Err<Error>` otherwise.
59
+ *
60
+ * This is a stricter alternative to both `parseInt` and `Number`:
61
+ *
62
+ * - Unlike `parseInt('12abc', 10)` (which returns `12`), trailing
63
+ * non-numeric characters make the whole input invalid and yield `Err`.
64
+ * - Unlike `Number('')` / `Number(' ')` (which return `0`), empty or
65
+ * whitespace-only input yields `Err`.
66
+ *
67
+ * The empty-string case is rejected by delegating to `parseInt` (which
68
+ * returns `NaN` there) rather than hard-coding a check, while the trailing-
69
+ * garbage case is rejected via `Number`. Valid input is truncated toward
70
+ * zero, so `'12.9'` becomes `12` and `'-3.5'` becomes `-3`.
71
+ *
72
+ * Only base 10 is supported. Use `Result.unwrapOk` (optionally with a
73
+ * `?? Number.NaN` fallback) or `Result.unwrapOkOr` to get a plain number
74
+ * back.
75
+ *
76
+ * @example
77
+ *
78
+ * ```ts
79
+ * assert.strictEqual(
80
+ * Result.unwrapOkOr(Num.safeParseInt('123'), Number.NaN),
81
+ * 123,
82
+ * );
83
+ *
84
+ * assert.strictEqual(
85
+ * Result.unwrapOkOr(Num.safeParseInt('12.9'), Number.NaN),
86
+ * 12,
87
+ * );
88
+ *
89
+ * assert.strictEqual(
90
+ * Result.unwrapOkOr(Num.safeParseInt('-12.9'), Number.NaN),
91
+ * -12,
92
+ * );
93
+ *
94
+ * assert.strictEqual(Number.parseInt('-12.9', 10), -12);
95
+ *
96
+ * // Native `parseInt` ignores trailing non-numeric characters
97
+ *
98
+ * assert.strictEqual(Number.parseInt('123abc', 10), 123);
99
+ *
100
+ * assert.isTrue(Number.isNaN(Number('123abc')));
101
+ *
102
+ * assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
103
+ *
104
+ * // Whitespace is not a valid integer, so we return an error instead of coercing to 0.
105
+ *
106
+ * assert.isTrue(Number.isNaN(Number.parseInt(' ', 10)));
107
+ *
108
+ * assert.strictEqual(Number(' '), 0); // Native `Number` coerces whitespace to 0
109
+ *
110
+ * assert.isTrue(Result.isErr(Num.safeParseInt('')));
111
+ *
112
+ * assert.strictEqual(Result.unwrapOk(Num.safeParseInt(' ')), undefined);
113
+ * ```
114
+ *
115
+ * @param s The string to parse.
116
+ * @returns `Result.ok(parsedInt)` for valid input, otherwise `Result.err`
117
+ * wrapping an `Error` describing the invalid input.
118
+ */
119
+ export const safeParseInt = (s: string): Result<Int, Error> => {
120
+ const viaNumber = Number(s);
121
+
122
+ // `Number('')` / `Number(' ')` は 0 を返すが、`parseInt` は NaN を返す。
123
+ // 末尾不正文字 ('12abc' 等) は `Number` 側が NaN にするので、両者が共に
124
+ // 有効な場合のみ採用することで空文字・空白のみ・末尾不正をまとめて弾く。
125
+ return Number.isNaN(viaNumber) || Number.isNaN(Number.parseInt(s, 10))
126
+ ? Result.err(
127
+ new Error(`safeParseInt: "${s}" is not a valid base-10 integer`),
128
+ )
129
+ : // eslint-disable-next-line total-functions/no-unsafe-type-assertion, ts-data-forge/prefer-as-int
130
+ Result.ok(Math.trunc(viaNumber) as Int);
131
+ };
132
+
55
133
  /**
56
134
  * Type guard that checks if a number is non-zero.
57
135
  *
@@ -1,6 +1,6 @@
1
1
  import { type NonZeroNumber } from 'ts-type-forge';
2
2
  import { expectType } from '../expect-type.mjs';
3
- import { pipe } from '../functional/index.mjs';
3
+ import { pipe, Result } from '../functional/index.mjs';
4
4
  import { asNonZeroFiniteNumber } from './branded-types/index.mjs';
5
5
  import { Num } from './num.mjs';
6
6
 
@@ -132,6 +132,64 @@ describe('Num test', () => {
132
132
  });
133
133
  });
134
134
 
135
+ describe('safeParseInt', () => {
136
+ test('parses valid integer strings into Ok', () => {
137
+ expect(Result.unwrapOk(Num.safeParseInt('123'))).toBe(123);
138
+
139
+ expect(Result.unwrapOk(Num.safeParseInt('-42'))).toBe(-42);
140
+
141
+ expect(Result.unwrapOk(Num.safeParseInt('+7'))).toBe(7);
142
+
143
+ expect(Result.unwrapOk(Num.safeParseInt('0'))).toBe(0);
144
+
145
+ expect(Result.unwrapOk(Num.safeParseInt(' 12 '))).toBe(12);
146
+ });
147
+
148
+ test('truncates non-integer numeric strings toward zero', () => {
149
+ expect(Result.unwrapOk(Num.safeParseInt('12.9'))).toBe(12);
150
+
151
+ expect(Result.unwrapOk(Num.safeParseInt('-3.5'))).toBe(-3);
152
+
153
+ expect(Result.unwrapOk(Num.safeParseInt('1e3'))).toBe(1000);
154
+ });
155
+
156
+ test('rejects trailing non-numeric characters (unlike parseInt)', () => {
157
+ assert.isTrue(Result.isErr(Num.safeParseInt('123abc')));
158
+
159
+ assert.isTrue(Result.isErr(Num.safeParseInt('12px')));
160
+
161
+ assert.isTrue(Result.isErr(Num.safeParseInt('abc')));
162
+ });
163
+
164
+ test('rejects empty / whitespace-only input (unlike Number)', () => {
165
+ assert.isTrue(Result.isErr(Num.safeParseInt('')));
166
+
167
+ assert.isTrue(Result.isErr(Num.safeParseInt(' ')));
168
+ });
169
+
170
+ test('rejects non-finite words', () => {
171
+ assert.isTrue(Result.isErr(Num.safeParseInt('Infinity')));
172
+
173
+ assert.isTrue(Result.isErr(Num.safeParseInt('NaN')));
174
+ });
175
+
176
+ test('the Err carries a descriptive Error', () => {
177
+ const result = Num.safeParseInt('nope');
178
+
179
+ assert.isTrue(Result.isErr(result));
180
+
181
+ if (Result.isErr(result)) {
182
+ expect(Result.unwrapErr(result)).toBeInstanceOf(Error);
183
+ }
184
+ });
185
+
186
+ test('composes with Result.unwrapOk + nullish fallback', () => {
187
+ expect(Result.unwrapOk(Num.safeParseInt('42')) ?? Number.NaN).toBe(42);
188
+
189
+ expect(Result.unwrapOk(Num.safeParseInt('')) ?? Number.NaN).toBeNaN();
190
+ });
191
+ });
192
+
135
193
  describe('isInRange', () => {
136
194
  test('checks range (lower inclusive, upper exclusive)', () => {
137
195
  const inRange = Num.isInRange(0, 10);
@@ -28,7 +28,7 @@ describe(debounce, () => {
28
28
 
29
29
  vi.advanceTimersByTime(50);
30
30
 
31
- expect(func).toHaveBeenCalledOnce();
31
+ expect(func).toHaveBeenCalledExactlyOnceWith();
32
32
  });
33
33
 
34
34
  test('should only execute the function once after multiple rapid calls', () => {
@@ -46,7 +46,7 @@ describe(debounce, () => {
46
46
 
47
47
  vi.advanceTimersByTime(100);
48
48
 
49
- expect(func).toHaveBeenCalledOnce();
49
+ expect(func).toHaveBeenCalledExactlyOnceWith();
50
50
  });
51
51
 
52
52
  test('should pass the arguments to the original function', () => {
@@ -9,12 +9,12 @@ describe(memoizeFunction, () => {
9
9
  // First call
10
10
  expect(memoized(5)).toBe(10);
11
11
 
12
- expect(mockFn).toHaveBeenCalledOnce();
12
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(5);
13
13
 
14
14
  // Second call with same argument - should use cache
15
15
  expect(memoized(5)).toBe(10);
16
16
 
17
- expect(mockFn).toHaveBeenCalledOnce();
17
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(5);
18
18
 
19
19
  // Call with different argument
20
20
  expect(memoized(3)).toBe(6);
@@ -29,11 +29,11 @@ describe(memoizeFunction, () => {
29
29
 
30
30
  expect(memoized(2, 3)).toBe(5);
31
31
 
32
- expect(mockFn).toHaveBeenCalledOnce();
32
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(2, 3);
33
33
 
34
34
  expect(memoized(2, 3)).toBe(5);
35
35
 
36
- expect(mockFn).toHaveBeenCalledOnce();
36
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(2, 3);
37
37
 
38
38
  expect(memoized(3, 2)).toBe(5);
39
39
 
@@ -48,14 +48,14 @@ describe(memoizeFunction, () => {
48
48
  // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
49
49
  expect(memoized(5)).toBeUndefined();
50
50
 
51
- expect(mockFn).toHaveBeenCalledOnce();
51
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(5);
52
52
 
53
53
  // Should use cache even for undefined
54
54
 
55
55
  // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
56
56
  expect(memoized(5)).toBeUndefined();
57
57
 
58
- expect(mockFn).toHaveBeenCalledOnce();
58
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(5);
59
59
  });
60
60
 
61
61
  test('should work with object arguments using primitive cache keys', () => {
@@ -73,12 +73,12 @@ describe(memoizeFunction, () => {
73
73
 
74
74
  expect(memoized(user1)).toBe('Hello Alice');
75
75
 
76
- expect(mockFn).toHaveBeenCalledOnce();
76
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(user1);
77
77
 
78
78
  // Same id, should use cache (even though name is different)
79
79
  expect(memoized(user2)).toBe('Hello Alice');
80
80
 
81
- expect(mockFn).toHaveBeenCalledOnce();
81
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(user1);
82
82
 
83
83
  // Different id, should call function
84
84
  expect(memoized(user3)).toBe('Hello Charlie');
@@ -133,11 +133,11 @@ describe(memoizeFunction, () => {
133
133
 
134
134
  expect(memoized(null)).toBe('default');
135
135
 
136
- expect(mockFn).toHaveBeenCalledOnce();
136
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(null);
137
137
 
138
138
  expect(memoized(null)).toBe('default');
139
139
 
140
- expect(mockFn).toHaveBeenCalledOnce();
140
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(null);
141
141
 
142
142
  expect(memoized(undefined)).toBe('default');
143
143
 
@@ -156,19 +156,19 @@ describe(memoizeFunction, () => {
156
156
  // First call
157
157
  const result1 = memoized();
158
158
 
159
- expect(mockFn).toHaveBeenCalledOnce();
159
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith();
160
160
 
161
161
  // Second call - should use cache and return the same random value
162
162
  const result2 = memoized();
163
163
 
164
- expect(mockFn).toHaveBeenCalledOnce();
164
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith();
165
165
 
166
166
  expect(result2).toBe(result1);
167
167
 
168
168
  // Third call - still using cache
169
169
  const result3 = memoized();
170
170
 
171
- expect(mockFn).toHaveBeenCalledOnce();
171
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith();
172
172
 
173
173
  expect(result3).toBe(result1);
174
174
  });
@@ -186,18 +186,18 @@ describe(memoizeFunction, () => {
186
186
 
187
187
  expect(memoized2(5)).toBe(15);
188
188
 
189
- expect(fn1).toHaveBeenCalledOnce();
189
+ expect(fn1).toHaveBeenCalledExactlyOnceWith(5);
190
190
 
191
- expect(fn2).toHaveBeenCalledOnce();
191
+ expect(fn2).toHaveBeenCalledExactlyOnceWith(5);
192
192
 
193
193
  // Each has its own cache
194
194
  expect(memoized1(5)).toBe(10);
195
195
 
196
196
  expect(memoized2(5)).toBe(15);
197
197
 
198
- expect(fn1).toHaveBeenCalledOnce();
198
+ expect(fn1).toHaveBeenCalledExactlyOnceWith(5);
199
199
 
200
- expect(fn2).toHaveBeenCalledOnce();
200
+ expect(fn2).toHaveBeenCalledExactlyOnceWith(5);
201
201
  });
202
202
 
203
203
  test('should work with complex cache key generation', () => {
@@ -236,12 +236,12 @@ describe(memoizeFunction, () => {
236
236
 
237
237
  expect(memoized(args1)).toBe('books/fiction/123');
238
238
 
239
- expect(mockFn).toHaveBeenCalledOnce();
239
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(args1);
240
240
 
241
241
  // Same cache key, should use cache
242
242
  expect(memoized(args2)).toBe('books/fiction/123');
243
243
 
244
- expect(mockFn).toHaveBeenCalledOnce();
244
+ expect(mockFn).toHaveBeenCalledExactlyOnceWith(args1);
245
245
 
246
246
  // Different id, different cache key
247
247
  expect(memoized(args3)).toBe('books/fiction/124');