Skip to content

Commit 1d8c439

Browse files
committed
Add orNull method to Evolu Type
Introduces the orNull method to the Type interface, allowing conversion of input to a nullable value when validation fails.
1 parent 5683994 commit 1d8c439

3 files changed

Lines changed: 56 additions & 10 deletions

File tree

.changeset/twenty-zebras-dance.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@evolu/common": patch
3+
---
4+
5+
Add `orNull` method to Evolu Type
6+
7+
Returns the validated value or `null` on failure. Useful when the error is not important and you just want the value or nothing.
8+
9+
```ts
10+
const age = PositiveInt.orNull(userInput) ?? 0;
11+
```

packages/common/src/Type.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ import { pack } from "msgpackr";
179179
import type { Brand } from "./Brand.js";
180180
import { type RandomBytesDep } from "./Crypto.js";
181181
import { isPlainObject } from "./Object.js";
182-
import { err, getOrThrow, ok, Result, trySync } from "./Result.js";
182+
import { err, getOrNull, getOrThrow, ok, Result, trySync } from "./Result.js";
183183
import { safelyStringifyUnknownValue } from "./String.js";
184184
import type { Literal, Simplify, WidenLiteral } from "./Types.js";
185185
import { IntentionalNever } from "./Types.js";
@@ -219,14 +219,6 @@ export interface Type<
219219
* constants)
220220
* - Application startup where failure should crash the program
221221
* - Test code with known valid inputs
222-
* - Converting from trusted sources where validation failure indicates a
223-
* programming error
224-
*
225-
* **When NOT to use:**
226-
*
227-
* - User input validation - use `from` and handle errors gracefully
228-
* - Data from external APIs or files - use `from` for proper error handling
229-
* - Library code that should return Results rather than throw
230222
*
231223
* ### Example
232224
*
@@ -249,6 +241,38 @@ export interface Type<
249241
*/
250242
readonly orThrow: (value: Input) => T;
251243

244+
/**
245+
* Creates `T` from an `Input` value, returning `null` if validation fails.
246+
*
247+
* This is a convenience method that combines `from` with `getOrNull`.
248+
*
249+
* **When to use:**
250+
*
251+
* - When you need to convert a validation result to a nullable value
252+
* - When the error is not important and you just want the value or nothing
253+
* - APIs that expect `T | null`
254+
*
255+
* ### Example
256+
*
257+
* ```ts
258+
* // ✅ Good: Optional user input
259+
* const age = PositiveInt.orNull(userInput);
260+
* if (age != null) {
261+
* console.log("Valid age:", age);
262+
* }
263+
*
264+
* // ✅ Good: Default fallback
265+
* const maxRetries = PositiveInt.orNull(config.retries) ?? 3;
266+
*
267+
* // ❌ Avoid: When you need to know why validation failed (use `from` instead)
268+
* const result = PositiveInt.from(userInput);
269+
* if (!result.ok) {
270+
* console.error(formatPositiveError(result.error));
271+
* }
272+
* ```
273+
*/
274+
readonly orNull: (value: Input) => T | null;
275+
252276
/**
253277
* Creates `T` from an unknown value.
254278
*
@@ -468,6 +492,7 @@ const createType = <
468492
| "is"
469493
| "from"
470494
| "orThrow"
495+
| "orNull"
471496
| typeof EvoluTypeSymbol
472497
| "Type"
473498
| "Input"
@@ -481,7 +506,8 @@ const createType = <
481506
name,
482507
is: (value: unknown): value is T => definition.fromUnknown(value).ok,
483508
from: definition.fromUnknown,
484-
orThrow: (value: Input): T => getOrThrow(definition.fromUnknown(value)),
509+
orThrow: (value) => getOrThrow(definition.fromUnknown(value)),
510+
orNull: (value) => getOrNull(definition.fromUnknown(value)),
485511
[EvoluTypeSymbol]: true,
486512
Type: undefined as unknown as T,
487513
Input: undefined as unknown as Input,

packages/common/test/Type.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ test("orThrow", () => {
171171
);
172172
});
173173

174+
test("orNull", () => {
175+
expect(PositiveNumber.orNull(42)).toBe(42);
176+
expect(PositiveNumber.orNull(-5)).toBe(null);
177+
expect(PositiveNumber.orNull(0)).toBe(null);
178+
expect(String.orNull("hello")).toBe("hello");
179+
expect(NonEmptyString.orNull("")).toBe(null);
180+
expect(NonEmptyString.orNull("valid")).toBe("valid");
181+
});
182+
174183
test("brand", () => {
175184
// It's for fromParent test.
176185
let trimmedStringRefineCount = 0;

0 commit comments

Comments
 (0)