Skip to content

Commit f2a984b

Browse files
committed
feat: 🎸 add s.cast namespace for programmer-friendly coercion
- s.cast.boolean(): understands 'true'/'false', 'yes'/'no', 'on'/'off', '1'/'0' (case-insensitive); rejects null/undefined/unrecognised strings- s.cast.number(): trims whitespace from strings, accepts booleans (true→1, false→0); rejects null/undefined/empty strings- s.cast.string(): accepts strings, finite numbers, and booleans; rejects null/undefined/objects/NaN/Infinity- s.cast.date(): accepts ISO strings, finite timestamps, and Date objects; rejects null/undefined/booleans/empty strings- s.cast.json(schema): parses JSON strings before schema validation; non-string inputs pass through; malformed JSON returns a proper validation failure
1 parent 98bb3a5 commit f2a984b

3 files changed

Lines changed: 704 additions & 6 deletions

File tree

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ This small library provides a simple schema validation system for JavaScript/Typ
1717
- [Full Extensions](#full-extensions-esmjschemafull)
1818
- [API Reference Summary](#api-reference-summary)
1919
- [Schema Types](#schema-types)
20+
- [s.coerce](#scoerce)
21+
- [s.cast](#scast)
2022
- [Schema Methods](#schema-methods)
2123
- [parse](#parsevalue-parseoptions)
2224
- [safeParse](#safeparsevalue-parseoptions)
@@ -449,6 +451,16 @@ const schema = s.object({
449451
- `s.coerce.boolean()` - Coerce any value to boolean, then validate
450452
- `s.coerce.date()` - Coerce any value to Date, then validate (fails for invalid dates)
451453

454+
### Cast
455+
456+
Semantic casting that understands common string representations and rejects ambiguous inputs:
457+
458+
- `s.cast.boolean()` - Cast to boolean; understands `'true'/'false'`, `'yes'/'no'`, `'on'/'off'`, `'1'/'0'` (case-insensitive); rejects `null`/`undefined`/unrecognised strings
459+
- `s.cast.number()` - Cast to number; trims whitespace from strings, accepts booleans (`true`→1, `false`→0); rejects `null`/`undefined`/empty strings
460+
- `s.cast.string()` - Cast to string; accepts strings, finite numbers, and booleans; rejects `null`/`undefined`/objects/`NaN`/`Infinity`
461+
- `s.cast.date()` - Cast to Date; accepts ISO strings, finite timestamps, and existing Dates; rejects `null`/`undefined`/booleans/empty strings
462+
- `s.cast.json(schema)` - Parse a JSON string and validate the result against a schema; non-string inputs pass through directly; malformed JSON returns a proper validation failure
463+
452464
### Transformations
453465

454466
- `.transform(fn)` - Transform value
@@ -773,6 +785,83 @@ s.coerce.number().refine((v) => v > 0, { message: 'Must be positive' }).parse('5
773785
s.coerce.number({ message: 'Expected a numeric value' }).parse('bad'); // throws: Expected a numeric value
774786
```
775787

788+
#### `s.cast`
789+
790+
Programmer-friendly semantic casting. Unlike `s.coerce` (raw JS constructors), `s.cast` understands
791+
common string representations and rejects ambiguous inputs like `null`, `undefined`, and empty strings.
792+
793+
| Method | Accepted inputs | Rejects |
794+
|---|---|---|
795+
| `s.cast.string(options?)` | strings, finite numbers, booleans | `null`, `undefined`, objects, `NaN`, `Infinity` |
796+
| `s.cast.number(options?)` | numbers (incl. booleans `true`/`false`→1/0), trimmed numeric strings | `null`, `undefined`, empty strings, non-numeric strings |
797+
| `s.cast.boolean(options?)` | booleans, `1`/`0`, `'true'/'false'`, `'yes'/'no'`, `'on'/'off'`, `'1'/'0'` | `null`, `undefined`, unrecognised strings, other numbers |
798+
| `s.cast.date(options?)` | `Date` objects, ISO strings, finite integer timestamps | `null`, `undefined`, booleans, empty strings, invalid date strings |
799+
| `s.cast.json(schema, options?)` | JSON strings (parsed), any non-string value (pass-through) | malformed JSON strings |
800+
801+
**Key differences from `s.coerce`:**
802+
803+
| Input | `s.coerce.boolean()` | `s.cast.boolean()` |
804+
|---|---|---|
805+
| `'false'` | `true` (non-empty string!) | `false` |
806+
| `'yes'` / `'no'` | `true` / `true` | `true` / `false` |
807+
| `null` | `false` | throws |
808+
809+
| Input | `s.coerce.number()` | `s.cast.number()` |
810+
|---|---|---|
811+
| `null` | `0` | throws |
812+
| `''` | `0` | throws |
813+
814+
| Input | `s.coerce.string()` | `s.cast.string()` |
815+
|---|---|---|
816+
| `null` | `'null'` | throws |
817+
| `undefined` | `'undefined'` | throws |
818+
819+
```typescript
820+
// boolean
821+
s.cast.boolean().parse('false'); // false — unlike coerce!
822+
s.cast.boolean().parse('yes'); // true
823+
s.cast.boolean().parse('on'); // true
824+
s.cast.boolean().parse('OFF'); // false (case-insensitive)
825+
s.cast.boolean().parse(1); // true
826+
s.cast.boolean().parse(0); // false
827+
s.cast.boolean().parse('hello'); // throws: Cannot cast "hello" to boolean...
828+
s.cast.boolean().parse(null); // throws
829+
830+
// number
831+
s.cast.number().parse('42'); // 42
832+
s.cast.number().parse(' 3.14 '); // 3.14 — trims whitespace
833+
s.cast.number().parse(true); // 1
834+
s.cast.number().parse(false); // 0
835+
s.cast.number().parse(null); // throws: Cannot cast "null" to a number...
836+
s.cast.number().parse(''); // throws
837+
838+
// string
839+
s.cast.string().parse(123); // '123'
840+
s.cast.string().parse(true); // 'true'
841+
s.cast.string().parse(false); // 'false'
842+
s.cast.string().parse(null); // throws: Cannot cast "null" to string...
843+
s.cast.string().parse(NaN); // throws
844+
845+
// date
846+
s.cast.date().parse('2024-01-01'); // Date object
847+
s.cast.date().parse(1704067200000); // Date object
848+
s.cast.date().parse(null); // throws: Cannot cast "null" to a valid date.
849+
s.cast.date().parse(true); // throws
850+
851+
// All schema methods chain normally:
852+
s.cast.number().refine((v) => v > 0, { message: 'Must be positive' }).parse('5'); // 5
853+
854+
// Custom error message:
855+
s.cast.boolean({ message: 'Must be a boolean flag' }).parse('maybe'); // throws: Must be a boolean flag
856+
857+
// json
858+
s.cast.json(s.object({ name: s.string() })).parse('{"name":"Alice"}'); // { name: 'Alice' }
859+
s.cast.json(s.array(s.number())).parse('[1,2,3]'); // [1, 2, 3]
860+
s.cast.json(s.object({ name: s.string() })).parse({ name: 'Alice' }); // { name: 'Alice' } — pass-through
861+
s.cast.json(s.number()).safeParse('not json'); // { success: false, error: ... }
862+
s.cast.json(s.number(), { message: 'Invalid JSON' }).parse('bad'); // throws: Invalid JSON
863+
```
864+
776865
### Schema Methods
777866

778867
#### `parse(value, parseOptions?)`
@@ -1404,6 +1493,7 @@ const userSchema = s.object({
14041493
| Email validation | `.email()` built-in | Custom extension (see [Extending Schemas](#extending-schemas)) |
14051494
| Error format | Native Error | Plain object `{ success, error, errors }` |
14061495
| Coerce | `z.coerce.number()` | `s.coerce.number()` |
1496+
| Smart cast | No direct equivalent | `s.cast.number()` — rejects nulls, understands `'yes'/'no'`, etc. |
14071497

14081498
**Migration Tips:**
14091499

0 commit comments

Comments
 (0)