|
14 | 14 | - [JSON Schema](#json-schema) |
15 | 15 | - [Standard Schema](#standard-schema) |
16 | 16 | - [Defining schemas](#defining-schemas) |
| 17 | + - [Advanced schemas](#advanced-schemas) |
17 | 18 | - [Strings](#strings) |
18 | 19 | - [ISO datetimes](#iso-datetimes) |
19 | 20 | - [Numbers](#numbers) |
@@ -368,20 +369,40 @@ S.any; |
368 | 369 | // Never type |
369 | 370 | // Allows no values |
370 | 371 | S.never; |
| 372 | +``` |
| 373 | + |
| 374 | +### Advanced schemas |
| 375 | + |
| 376 | +The goal of **Sury** is to provide the best DX. To achieve that, everything is a schema — use it directly without a `()` call. However, some schemas are opt‑in to keep bundle size small, so you must enable them explicitly. This also helps prevent your team from using the wrong API. |
| 377 | + |
| 378 | +Enable the schemas you need at the project root: |
| 379 | + |
| 380 | +```ts |
| 381 | +S.enableJson(); |
| 382 | +S.enableJsonString(); |
| 383 | +``` |
371 | 384 |
|
| 385 | +And use them as usual: |
| 386 | + |
| 387 | +> 🧠 Don't forget `S.to` which comes with powerful coercion logic. |
| 388 | +
|
| 389 | +```ts |
372 | 390 | // JSON type |
373 | | -S.enableJson(); // ❕ Call at the project root. |
374 | 391 | // Allows string | boolean | number | null | Record<string, JSON> | JSON[] |
375 | 392 | S.json; |
376 | 393 |
|
377 | 394 | // JSON string |
378 | | -S.enableJsonString(); // ❕ Call at the project root. |
| 395 | + |
379 | 396 | // Asserts that the input is a valid JSON string |
380 | 397 | S.jsonString; |
381 | 398 | S.jsonStringWithSpace(2); |
| 399 | + |
382 | 400 | // Parses JSON string and validates that it's a number |
| 401 | +// JSON string -> number |
383 | 402 | S.jsonString.with(S.to, S.number); |
| 403 | + |
384 | 404 | // Serializes number to JSON string |
| 405 | +// number -> JSON string |
385 | 406 | S.number.with(S.to, S.jsonString); |
386 | 407 | ``` |
387 | 408 |
|
@@ -867,6 +888,41 @@ S.toJSONSchema(documentedStringSchema); |
867 | 888 | // } |
868 | 889 | ``` |
869 | 890 |
|
| 891 | +## Brand |
| 892 | + |
| 893 | +Add a type-only symbol to an existing type so that only values produced by validation satisfy it. |
| 894 | + |
| 895 | +Use `S.brand` to attach a nominal brand to a schema's output. This is a TypeScript-only marker: it does not change runtime behavior. Combine it with `S.refine` (or any validation) so only validated values can acquire the brand. |
| 896 | + |
| 897 | +```ts |
| 898 | +// Brand a string as a UserId |
| 899 | +const UserId = S.string.with(S.brand, "UserId"); |
| 900 | +type UserId = S.Infer<typeof UserId>; // S.Brand<string, "UserId"> |
| 901 | + |
| 902 | +const id: UserId = S.parseOrThrow("u_123", UserId); // OK |
| 903 | +const asString: string = id; // OK: branded value is assignable to string |
| 904 | +// @ts-expect-error - A plain string is not assignable to a branded string |
| 905 | +const notId: UserId = "u_123"; |
| 906 | +``` |
| 907 | + |
| 908 | +You can define brands for refined constraints, like even numbers: |
| 909 | + |
| 910 | +```ts |
| 911 | +const even = S.number |
| 912 | + .with(S.refine, (value, s) => { |
| 913 | + if (value % 2 !== 0) s.fail("Expected an even number"); |
| 914 | + }) |
| 915 | + .with(S.brand, "even"); |
| 916 | + |
| 917 | +type Even = S.Infer<typeof even>; // S.Brand<number, "even"> |
| 918 | + |
| 919 | +const good: Even = S.parseOrThrow(2, even); // OK |
| 920 | +// @ts-expect-error - number is not assignable to brand "even" |
| 921 | +const bad: Even = 5; |
| 922 | +``` |
| 923 | + |
| 924 | +For more information on branding in general, check out [this excellent article](https://www.learningtypescript.com/articles/branded-types) from [Josh Goldberg](https://github.com/joshuakgoldberg). |
| 925 | + |
870 | 926 | ## Custom schema |
871 | 927 |
|
872 | 928 | **Sury** might not have many built-in schemas for your use case. In this case you can create a custom schema for any TypeScript type. |
|
0 commit comments