ts-fortress 4.2.1 → 4.3.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.
Files changed (320) hide show
  1. package/README.md +424 -569
  2. package/dist/array/array-of-length.mjs +3 -3
  3. package/dist/array/array.mjs +1 -1
  4. package/dist/array/non-empty-array.mjs +1 -1
  5. package/dist/array/tuple.mjs +2 -2
  6. package/dist/brand/brand.d.mts.map +1 -0
  7. package/dist/{branded → brand}/brand.mjs +1 -1
  8. package/dist/brand/brand.mjs.map +1 -0
  9. package/dist/brand/branded-number.d.mts +13 -0
  10. package/dist/brand/branded-number.d.mts.map +1 -0
  11. package/dist/brand/branded-number.mjs +18 -0
  12. package/dist/brand/branded-number.mjs.map +1 -0
  13. package/dist/brand/branded-string.d.mts +13 -0
  14. package/dist/brand/branded-string.d.mts.map +1 -0
  15. package/dist/brand/branded-string.mjs +18 -0
  16. package/dist/brand/branded-string.mjs.map +1 -0
  17. package/dist/brand/index.d.mts +4 -0
  18. package/dist/brand/index.d.mts.map +1 -0
  19. package/dist/brand/index.mjs +4 -0
  20. package/dist/{branded → brand}/index.mjs.map +1 -1
  21. package/dist/compose/index.d.mts +0 -1
  22. package/dist/compose/index.d.mts.map +1 -1
  23. package/dist/compose/index.mjs +0 -1
  24. package/dist/compose/index.mjs.map +1 -1
  25. package/dist/compose/intersection.mjs +1 -1
  26. package/dist/compose/merge-records.mjs +2 -2
  27. package/dist/compose/union.mjs +2 -2
  28. package/dist/entry-point.mjs +21 -20
  29. package/dist/entry-point.mjs.map +1 -1
  30. package/dist/enum/enum.mjs +3 -3
  31. package/dist/enum/int-range.mjs +2 -2
  32. package/dist/enum/uint-range.mjs +2 -2
  33. package/dist/globals.d.mts +1 -1
  34. package/dist/index.d.mts +1 -1
  35. package/dist/index.d.mts.map +1 -1
  36. package/dist/index.mjs +21 -20
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/other-types/index.d.mts +1 -0
  39. package/dist/other-types/index.d.mts.map +1 -1
  40. package/dist/other-types/index.mjs +1 -0
  41. package/dist/other-types/index.mjs.map +1 -1
  42. package/dist/other-types/map.d.mts.map +1 -1
  43. package/dist/other-types/map.mjs +3 -2
  44. package/dist/other-types/map.mjs.map +1 -1
  45. package/dist/other-types/recursion.d.mts.map +1 -0
  46. package/dist/other-types/recursion.mjs.map +1 -0
  47. package/dist/other-types/set.d.mts.map +1 -1
  48. package/dist/other-types/set.mjs +3 -2
  49. package/dist/other-types/set.mjs.map +1 -1
  50. package/dist/predefined/brand/index.d.mts +2 -0
  51. package/dist/predefined/brand/index.d.mts.map +1 -0
  52. package/dist/{branded → predefined/brand}/index.mjs +0 -3
  53. package/dist/predefined/brand/number/finite-number.d.mts +3 -0
  54. package/dist/predefined/brand/number/finite-number.d.mts.map +1 -0
  55. package/dist/{branded → predefined/brand}/number/finite-number.mjs +2 -2
  56. package/dist/predefined/brand/number/finite-number.mjs.map +1 -0
  57. package/dist/predefined/brand/number/index.d.mts.map +1 -0
  58. package/dist/predefined/brand/number/index.mjs.map +1 -0
  59. package/dist/predefined/brand/number/int.d.mts +3 -0
  60. package/dist/predefined/brand/number/int.d.mts.map +1 -0
  61. package/dist/{branded → predefined/brand}/number/int.mjs +2 -2
  62. package/dist/predefined/brand/number/int.mjs.map +1 -0
  63. package/dist/predefined/brand/number/int16.d.mts +3 -0
  64. package/dist/predefined/brand/number/int16.d.mts.map +1 -0
  65. package/dist/{branded → predefined/brand}/number/int16.mjs +2 -2
  66. package/dist/predefined/brand/number/int16.mjs.map +1 -0
  67. package/dist/predefined/brand/number/int32.d.mts +3 -0
  68. package/dist/predefined/brand/number/int32.d.mts.map +1 -0
  69. package/dist/{branded → predefined/brand}/number/int32.mjs +2 -2
  70. package/dist/predefined/brand/number/int32.mjs.map +1 -0
  71. package/dist/{branded → predefined/brand}/number/non-negative-finite-number.d.mts +1 -1
  72. package/dist/predefined/brand/number/non-negative-finite-number.d.mts.map +1 -0
  73. package/dist/{branded → predefined/brand}/number/non-negative-finite-number.mjs +2 -2
  74. package/dist/predefined/brand/number/non-negative-finite-number.mjs.map +1 -0
  75. package/dist/{branded → predefined/brand}/number/non-zero-finite-number.d.mts +1 -1
  76. package/dist/predefined/brand/number/non-zero-finite-number.d.mts.map +1 -0
  77. package/dist/{branded → predefined/brand}/number/non-zero-finite-number.mjs +2 -2
  78. package/dist/predefined/brand/number/non-zero-finite-number.mjs.map +1 -0
  79. package/dist/predefined/brand/number/non-zero-int.d.mts +3 -0
  80. package/dist/predefined/brand/number/non-zero-int.d.mts.map +1 -0
  81. package/dist/{branded → predefined/brand}/number/non-zero-int.mjs +2 -2
  82. package/dist/predefined/brand/number/non-zero-int.mjs.map +1 -0
  83. package/dist/predefined/brand/number/non-zero-safe-int.d.mts +3 -0
  84. package/dist/predefined/brand/number/non-zero-safe-int.d.mts.map +1 -0
  85. package/dist/{branded → predefined/brand}/number/non-zero-safe-int.mjs +2 -2
  86. package/dist/predefined/brand/number/non-zero-safe-int.mjs.map +1 -0
  87. package/dist/{branded → predefined/brand}/number/positive-finite-number.d.mts +1 -1
  88. package/dist/predefined/brand/number/positive-finite-number.d.mts.map +1 -0
  89. package/dist/{branded → predefined/brand}/number/positive-finite-number.mjs +2 -2
  90. package/dist/predefined/brand/number/positive-finite-number.mjs.map +1 -0
  91. package/dist/predefined/brand/number/positive-int.d.mts +3 -0
  92. package/dist/predefined/brand/number/positive-int.d.mts.map +1 -0
  93. package/dist/{branded → predefined/brand}/number/positive-int.mjs +2 -2
  94. package/dist/predefined/brand/number/positive-int.mjs.map +1 -0
  95. package/dist/{branded → predefined/brand}/number/positive-safe-int.d.mts +1 -1
  96. package/dist/predefined/brand/number/positive-safe-int.d.mts.map +1 -0
  97. package/dist/{branded → predefined/brand}/number/positive-safe-int.mjs +2 -2
  98. package/dist/predefined/brand/number/positive-safe-int.mjs.map +1 -0
  99. package/dist/predefined/brand/number/safe-int.d.mts +3 -0
  100. package/dist/predefined/brand/number/safe-int.d.mts.map +1 -0
  101. package/dist/{branded → predefined/brand}/number/safe-int.mjs +2 -2
  102. package/dist/predefined/brand/number/safe-int.mjs.map +1 -0
  103. package/dist/predefined/brand/number/safe-uint.d.mts +3 -0
  104. package/dist/predefined/brand/number/safe-uint.d.mts.map +1 -0
  105. package/dist/{branded → predefined/brand}/number/safe-uint.mjs +2 -2
  106. package/dist/predefined/brand/number/safe-uint.mjs.map +1 -0
  107. package/dist/predefined/brand/number/uint.d.mts +3 -0
  108. package/dist/predefined/brand/number/uint.d.mts.map +1 -0
  109. package/dist/{branded → predefined/brand}/number/uint.mjs +2 -2
  110. package/dist/predefined/brand/number/uint.mjs.map +1 -0
  111. package/dist/predefined/brand/number/uint16.d.mts +3 -0
  112. package/dist/predefined/brand/number/uint16.d.mts.map +1 -0
  113. package/dist/{branded → predefined/brand}/number/uint16.mjs +2 -2
  114. package/dist/predefined/brand/number/uint16.mjs.map +1 -0
  115. package/dist/predefined/brand/number/uint32.d.mts +3 -0
  116. package/dist/predefined/brand/number/uint32.d.mts.map +1 -0
  117. package/dist/{branded → predefined/brand}/number/uint32.mjs +2 -2
  118. package/dist/predefined/brand/number/uint32.mjs.map +1 -0
  119. package/dist/predefined/index.d.mts +1 -0
  120. package/dist/predefined/index.d.mts.map +1 -1
  121. package/dist/predefined/index.mjs +16 -0
  122. package/dist/predefined/index.mjs.map +1 -1
  123. package/dist/predefined/json.d.mts.map +1 -1
  124. package/dist/predefined/json.mjs +2 -1
  125. package/dist/predefined/json.mjs.map +1 -1
  126. package/dist/record/index.d.mts +1 -0
  127. package/dist/record/index.d.mts.map +1 -1
  128. package/dist/record/index.mjs +1 -0
  129. package/dist/record/index.mjs.map +1 -1
  130. package/dist/record/key-value-record.mjs +3 -3
  131. package/dist/record/keyof.mjs +2 -2
  132. package/dist/record/optional.d.mts +1 -0
  133. package/dist/record/optional.d.mts.map +1 -1
  134. package/dist/record/optional.mjs.map +1 -1
  135. package/dist/record/partial.mjs +1 -1
  136. package/dist/record/record.mjs +3 -3
  137. package/dist/record/required.d.mts +22 -0
  138. package/dist/record/required.d.mts.map +1 -0
  139. package/dist/record/required.mjs +33 -0
  140. package/dist/record/required.mjs.map +1 -0
  141. package/dist/record/valueof.mjs +2 -2
  142. package/package.json +32 -28
  143. package/src/array/array-of-length.mts +3 -3
  144. package/src/array/array-of-length.test.mts +42 -46
  145. package/src/array/array.mts +1 -1
  146. package/src/array/array.test.mts +30 -33
  147. package/src/array/non-empty-array.mts +1 -1
  148. package/src/array/non-empty-array.test.mts +42 -46
  149. package/src/array/tuple.mts +2 -2
  150. package/src/array/tuple.test.mts +52 -57
  151. package/src/{branded → brand}/brand.mts +1 -1
  152. package/src/{branded/simple-branded-number.mts → brand/branded-number.mts} +6 -2
  153. package/src/{branded/simple-branded-number.test.mts → brand/branded-number.test.mts} +43 -47
  154. package/src/{branded/simple-branded-string.mts → brand/branded-string.mts} +6 -2
  155. package/src/{branded/simple-branded-string.test.mts → brand/branded-string.test.mts} +45 -49
  156. package/src/brand/index.mts +3 -0
  157. package/src/compose/index.mts +0 -1
  158. package/src/compose/intersection.mts +1 -1
  159. package/src/compose/intersection.test.mts +122 -131
  160. package/src/compose/merge-records.mts +2 -2
  161. package/src/compose/merge-records.options.test.mts +37 -0
  162. package/src/compose/merge-records.test.mts +34 -38
  163. package/src/compose/union.mts +2 -2
  164. package/src/compose/union.test.mts +16 -20
  165. package/src/enum/enum.mts +3 -3
  166. package/src/enum/enum.test.mts +15 -18
  167. package/src/enum/int-range.mts +2 -2
  168. package/src/enum/int-range.test.mts +69 -0
  169. package/src/enum/uint-range.mts +2 -2
  170. package/src/enum/uint-range.test.mts +15 -18
  171. package/src/globals.d.mts +1 -1
  172. package/src/index.mts +1 -1
  173. package/src/other-types/index.mts +1 -0
  174. package/src/other-types/literal.test.mts +108 -125
  175. package/src/other-types/map.mts +6 -3
  176. package/src/other-types/map.test.mts +19 -23
  177. package/src/other-types/recursion.cache.test.mts +42 -0
  178. package/src/{compose → other-types}/recursion.test.mts +2 -2
  179. package/src/other-types/set.mts +6 -3
  180. package/src/other-types/set.test.mts +20 -24
  181. package/src/predefined/brand/index.mts +1 -0
  182. package/src/{branded → predefined/brand}/number/finite-number.mts +3 -3
  183. package/src/{branded → predefined/brand}/number/finite-number.test.mts +30 -34
  184. package/src/{branded → predefined/brand}/number/int.mts +3 -3
  185. package/src/{branded → predefined/brand}/number/int.test.mts +34 -38
  186. package/src/{branded → predefined/brand}/number/int16.mts +3 -3
  187. package/src/{branded → predefined/brand}/number/int16.test.mts +36 -35
  188. package/src/{branded → predefined/brand}/number/int32.mts +3 -3
  189. package/src/{branded → predefined/brand}/number/int32.test.mts +39 -38
  190. package/src/{branded → predefined/brand}/number/non-negative-finite-number.mts +3 -3
  191. package/src/{branded → predefined/brand}/number/non-negative-finite-number.test.mts +30 -28
  192. package/src/{branded → predefined/brand}/number/non-zero-finite-number.mts +3 -3
  193. package/src/{branded → predefined/brand}/number/non-zero-finite-number.test.mts +36 -29
  194. package/src/{branded → predefined/brand}/number/non-zero-int.mts +3 -3
  195. package/src/{branded → predefined/brand}/number/non-zero-int.test.mts +36 -28
  196. package/src/{branded → predefined/brand}/number/non-zero-safe-int.mts +3 -3
  197. package/src/{branded → predefined/brand}/number/non-zero-safe-int.test.mts +37 -35
  198. package/src/{branded → predefined/brand}/number/positive-finite-number.mts +3 -3
  199. package/src/{branded → predefined/brand}/number/positive-finite-number.test.mts +36 -30
  200. package/src/{branded → predefined/brand}/number/positive-int.mts +3 -3
  201. package/src/{branded → predefined/brand}/number/positive-int.test.mts +51 -46
  202. package/src/{branded → predefined/brand}/number/positive-safe-int.mts +3 -3
  203. package/src/{branded → predefined/brand}/number/positive-safe-int.test.mts +33 -33
  204. package/src/{branded → predefined/brand}/number/safe-int.mts +3 -3
  205. package/src/{branded → predefined/brand}/number/safe-int.test.mts +20 -23
  206. package/src/{branded → predefined/brand}/number/safe-uint.mts +3 -3
  207. package/src/{branded → predefined/brand}/number/safe-uint.test.mts +43 -36
  208. package/src/{branded → predefined/brand}/number/uint.mts +3 -3
  209. package/src/{branded → predefined/brand}/number/uint.test.mts +51 -47
  210. package/src/{branded → predefined/brand}/number/uint16.mts +3 -3
  211. package/src/{branded → predefined/brand}/number/uint16.test.mts +44 -37
  212. package/src/{branded → predefined/brand}/number/uint32.mts +3 -3
  213. package/src/{branded → predefined/brand}/number/uint32.test.mts +44 -37
  214. package/src/predefined/index.mts +1 -0
  215. package/src/predefined/int8.test.mts +25 -0
  216. package/src/predefined/json.mts +2 -1
  217. package/src/predefined/uint8.test.mts +25 -0
  218. package/src/primitives/bigint.literal.test.mts +23 -0
  219. package/src/primitives/bigint.test.mts +18 -21
  220. package/src/primitives/boolean.literal.test.mts +23 -0
  221. package/src/primitives/boolean.test.mts +18 -21
  222. package/src/primitives/null.test.mts +24 -28
  223. package/src/primitives/number.literal.test.mts +23 -0
  224. package/src/primitives/number.test.mts +15 -18
  225. package/src/primitives/string.literal.test.mts +23 -0
  226. package/src/primitives/string.test.mts +17 -21
  227. package/src/primitives/symbol.test.mts +18 -21
  228. package/src/primitives/undefined.test.mts +24 -28
  229. package/src/record/index.mts +1 -0
  230. package/src/record/key-value-record.mts +3 -3
  231. package/src/record/key-value-record.test.mts +46 -49
  232. package/src/record/keyof.empty.test.mts +14 -0
  233. package/src/record/keyof.mts +2 -2
  234. package/src/record/keyof.test.mts +19 -22
  235. package/src/record/omit.options.test.mts +24 -0
  236. package/src/record/omit.test.mts +30 -35
  237. package/src/record/optional.mts +5 -0
  238. package/src/record/partial.mts +2 -2
  239. package/src/record/partial.test.mts +83 -94
  240. package/src/record/pick.options.test.mts +24 -0
  241. package/src/record/pick.test.mts +30 -35
  242. package/src/record/record-allow-excess-properties.test.mts +79 -88
  243. package/src/record/record-strict-composition-simple.test.mts +37 -48
  244. package/src/record/record-strict-composition.test.mts +136 -163
  245. package/src/record/record.mts +3 -3
  246. package/src/record/record.test.mts +80 -87
  247. package/src/record/required.mts +246 -0
  248. package/src/record/required.test.mts +587 -0
  249. package/src/record/strict-record.test.mts +52 -58
  250. package/src/record/valueof.mts +2 -2
  251. package/src/record/valueof.test.mts +24 -166
  252. package/src/utils/validation-error.formatting.test.mts +47 -0
  253. package/dist/branded/brand.d.mts.map +0 -1
  254. package/dist/branded/brand.mjs.map +0 -1
  255. package/dist/branded/index.d.mts +0 -5
  256. package/dist/branded/index.d.mts.map +0 -1
  257. package/dist/branded/number/finite-number.d.mts +0 -3
  258. package/dist/branded/number/finite-number.d.mts.map +0 -1
  259. package/dist/branded/number/finite-number.mjs.map +0 -1
  260. package/dist/branded/number/index.d.mts.map +0 -1
  261. package/dist/branded/number/int.d.mts +0 -3
  262. package/dist/branded/number/int.d.mts.map +0 -1
  263. package/dist/branded/number/int.mjs.map +0 -1
  264. package/dist/branded/number/int16.d.mts +0 -3
  265. package/dist/branded/number/int16.d.mts.map +0 -1
  266. package/dist/branded/number/int16.mjs.map +0 -1
  267. package/dist/branded/number/int32.d.mts +0 -3
  268. package/dist/branded/number/int32.d.mts.map +0 -1
  269. package/dist/branded/number/int32.mjs.map +0 -1
  270. package/dist/branded/number/non-negative-finite-number.d.mts.map +0 -1
  271. package/dist/branded/number/non-negative-finite-number.mjs.map +0 -1
  272. package/dist/branded/number/non-zero-finite-number.d.mts.map +0 -1
  273. package/dist/branded/number/non-zero-finite-number.mjs.map +0 -1
  274. package/dist/branded/number/non-zero-int.d.mts +0 -3
  275. package/dist/branded/number/non-zero-int.d.mts.map +0 -1
  276. package/dist/branded/number/non-zero-int.mjs.map +0 -1
  277. package/dist/branded/number/non-zero-safe-int.d.mts +0 -3
  278. package/dist/branded/number/non-zero-safe-int.d.mts.map +0 -1
  279. package/dist/branded/number/non-zero-safe-int.mjs.map +0 -1
  280. package/dist/branded/number/positive-finite-number.d.mts.map +0 -1
  281. package/dist/branded/number/positive-finite-number.mjs.map +0 -1
  282. package/dist/branded/number/positive-int.d.mts +0 -3
  283. package/dist/branded/number/positive-int.d.mts.map +0 -1
  284. package/dist/branded/number/positive-int.mjs.map +0 -1
  285. package/dist/branded/number/positive-safe-int.d.mts.map +0 -1
  286. package/dist/branded/number/positive-safe-int.mjs.map +0 -1
  287. package/dist/branded/number/safe-int.d.mts +0 -3
  288. package/dist/branded/number/safe-int.d.mts.map +0 -1
  289. package/dist/branded/number/safe-int.mjs.map +0 -1
  290. package/dist/branded/number/safe-uint.d.mts +0 -3
  291. package/dist/branded/number/safe-uint.d.mts.map +0 -1
  292. package/dist/branded/number/safe-uint.mjs.map +0 -1
  293. package/dist/branded/number/uint.d.mts +0 -3
  294. package/dist/branded/number/uint.d.mts.map +0 -1
  295. package/dist/branded/number/uint.mjs.map +0 -1
  296. package/dist/branded/number/uint16.d.mts +0 -3
  297. package/dist/branded/number/uint16.d.mts.map +0 -1
  298. package/dist/branded/number/uint16.mjs.map +0 -1
  299. package/dist/branded/number/uint32.d.mts +0 -3
  300. package/dist/branded/number/uint32.d.mts.map +0 -1
  301. package/dist/branded/number/uint32.mjs.map +0 -1
  302. package/dist/branded/simple-branded-number.d.mts +0 -7
  303. package/dist/branded/simple-branded-number.d.mts.map +0 -1
  304. package/dist/branded/simple-branded-number.mjs +0 -15
  305. package/dist/branded/simple-branded-number.mjs.map +0 -1
  306. package/dist/branded/simple-branded-string.d.mts +0 -7
  307. package/dist/branded/simple-branded-string.d.mts.map +0 -1
  308. package/dist/branded/simple-branded-string.mjs +0 -15
  309. package/dist/branded/simple-branded-string.mjs.map +0 -1
  310. package/dist/compose/recursion.d.mts.map +0 -1
  311. package/dist/compose/recursion.mjs.map +0 -1
  312. package/src/branded/index.mts +0 -4
  313. /package/dist/{branded → brand}/brand.d.mts +0 -0
  314. /package/dist/{compose → other-types}/recursion.d.mts +0 -0
  315. /package/dist/{compose → other-types}/recursion.mjs +0 -0
  316. /package/dist/{branded/number → predefined/brand}/index.mjs.map +0 -0
  317. /package/dist/{branded → predefined/brand}/number/index.d.mts +0 -0
  318. /package/dist/{branded → predefined/brand}/number/index.mjs +0 -0
  319. /package/src/{compose → other-types}/recursion.mts +0 -0
  320. /package/src/{branded → predefined/brand}/number/index.mts +0 -0
package/README.md CHANGED
@@ -35,7 +35,8 @@ pnpm add ts-fortress
35
35
 
36
36
  ## Quick Start
37
37
 
38
- ```typescript
38
+ ```tsx
39
+ import { expectType } from 'ts-data-forge';
39
40
  import * as t from 'ts-fortress';
40
41
 
41
42
  // Define a schema
@@ -43,19 +44,23 @@ const User = t.record({
43
44
  id: t.string(),
44
45
  name: t.string(),
45
46
  age: t.number(),
46
- email: t.string(),
47
+ email: t.optional(t.string()),
47
48
  isActive: t.boolean(),
48
49
  });
49
50
 
50
51
  // Infer TypeScript type
51
52
  type User = t.TypeOf<typeof User>;
52
- // ↑ Readonly<{
53
- // id: string;
54
- // name: string;
55
- // age: number;
56
- // email: string;
57
- // isActive: boolean
58
- // }>
53
+
54
+ expectType<
55
+ User,
56
+ Readonly<{
57
+ id: string;
58
+ name: string;
59
+ age: number;
60
+ email?: string;
61
+ isActive: boolean;
62
+ }>
63
+ >('=');
59
64
 
60
65
  // Validate data
61
66
  const userData = {
@@ -64,19 +69,29 @@ const userData = {
64
69
  age: 30,
65
70
  email: 'john@example.com',
66
71
  isActive: true,
67
- };
72
+ } as const;
73
+
74
+ assert(User.is(userData));
68
75
 
69
76
  if (User.is(userData)) {
70
77
  // userData is now typed as User
71
- console.log(`User: ${userData.name}, Age: ${userData.age}`);
78
+ userData satisfies User;
79
+
80
+ assert.equal(
81
+ `User: ${userData.name}, Age: ${userData.age}`,
82
+ 'User: John Doe, Age: 30',
83
+ );
72
84
  }
73
85
 
74
86
  // Get validation result with error details
75
87
  const result = User.validate(userData);
76
88
  if (t.Result.isOk(result)) {
77
- const user = result.value; // typed as User
89
+ result.value satisfies User; // typed as User
78
90
  } else {
79
- console.error('Validation errors:', result.value); // readonly ValidationError[] showing error information
91
+ console.error(
92
+ 'Validation errors:',
93
+ result.value satisfies readonly t.ValidationError[],
94
+ );
80
95
  }
81
96
  ```
82
97
 
@@ -84,19 +99,19 @@ if (t.Result.isOk(result)) {
84
99
 
85
100
  One of the key design decisions in ts-fortress is that **all schema types have explicit default values**, which allows for powerful data entry capabilities:
86
101
 
87
- ```typescript
102
+ ```tsx
88
103
  import * as t from 'ts-fortress';
89
104
 
90
105
  // Every type requires a default value
91
106
  const UserProfile = t.record({
92
107
  name: t.string('Anonymous'), // Default: 'Anonymous'
93
- age: t.number(0), // Default: 0
94
- email: t.optional(t.string('')), // Optional field with default ''
108
+ age: t.number(), // Default: 0
109
+ email: t.optional(t.string()), // Optional field with default ''
95
110
  preferences: t.record({
96
111
  theme: t.string('light'), // Default: 'light'
97
112
  notifications: t.boolean(true), // Default: true
98
113
  }),
99
- tags: t.array(t.string('')), // Default: empty array []
114
+ tags: t.array(t.string()), // Default: empty array []
100
115
  });
101
116
 
102
117
  // The fill() function automatically provides missing values
@@ -110,29 +125,39 @@ const partialData = {
110
125
  };
111
126
 
112
127
  const filledData = UserProfile.fill(partialData);
113
- console.log(filledData);
114
- // Output: {
115
- // name: 'John Doe',
116
- // age: 0, // ← Filled with default
117
- // email: '', // ← Filled with default
118
- // preferences: {
119
- // theme: 'dark',
120
- // notifications: true, // ← Filled with default
121
- // },
122
- // tags: [], // ← Filled with default
123
- // }
128
+
129
+ assert.deepStrictEqual(filledData, {
130
+ name: 'John Doe',
131
+ age: 0, // ← Filled with default
132
+ email: '', // ← Filled with default
133
+ preferences: {
134
+ theme: 'dark',
135
+ notifications: true, // ← Filled with default
136
+ },
137
+ tags: [], // ← Filled with default
138
+ });
124
139
 
125
140
  // fill() is type-safe and always returns a complete object
126
141
  type UserProfile = t.TypeOf<typeof UserProfile>;
127
- const completeUser: UserProfile = UserProfile.fill(anyPartialData);
128
142
 
129
143
  // Important: Default value filling only occurs when fill() is called
130
144
  // The is() and validate() functions can still detect missing keys
131
- console.log(UserProfile.is(partialData)); // false - missing required keys
145
+ assert(!UserProfile.is(partialData)); // missing required keys
146
+
132
147
  const result = UserProfile.validate(partialData);
133
- if (t.Result.isErr(result)) {
134
- console.log(result.value); // ValidationError[] showing missing keys
135
- }
148
+
149
+ assert(t.Result.isErr(result));
150
+
151
+ assert.deepStrictEqual(
152
+ t.validationErrorsToMessages(
153
+ result.value satisfies readonly t.ValidationError[],
154
+ ),
155
+ [
156
+ `Missing required key "age" at age`,
157
+ `Missing required key "notifications" at preferences.notifications`,
158
+ `Missing required key "tags" at tags`,
159
+ ],
160
+ );
136
161
  ```
137
162
 
138
163
  ### Benefits of Default Values
@@ -148,7 +173,9 @@ if (t.Result.isErr(result)) {
148
173
 
149
174
  Most ts-fortress types provide sensible defaults automatically, so you rarely need to specify explicit default values:
150
175
 
151
- ```typescript
176
+ ```tsx
177
+ import * as t from 'ts-fortress';
178
+
152
179
  // Most common types have built-in defaults
153
180
  const Schema = t.record({
154
181
  name: t.string(), // defaults to ""
@@ -156,14 +183,16 @@ const Schema = t.record({
156
183
  active: t.boolean(), // defaults to false
157
184
  tags: t.array(t.string()), // defaults to []
158
185
  config: t.record({
159
- debug: t.boolean(), // defaults to false
186
+ debug: t.nullable(t.boolean()), // defaults to false
160
187
  }), // defaults to { debug: false }
161
188
  });
162
189
  ```
163
190
 
164
191
  You only need to specify explicit default values in two cases: when you want custom values, or when using `intersection` types:
165
192
 
166
- ```typescript
193
+ ```tsx
194
+ import * as t from 'ts-fortress';
195
+
167
196
  // Custom default values
168
197
  const ServerConfig = t.record({
169
198
  port: t.number(3000), // custom default: 3000
@@ -171,22 +200,32 @@ const ServerConfig = t.record({
171
200
  retries: t.number(5), // custom default: 5
172
201
  });
173
202
 
174
- const JobStatus = t.enumType(['started', 'scheduled', 'succeeded', 'failed']);
203
+ assert.deepStrictEqual(ServerConfig.defaultValue, {
204
+ port: 3000,
205
+ host: 'localhost',
206
+ retries: 5,
207
+ } satisfies t.TypeOf<typeof ServerConfig>);
208
+
209
+ // Enum types have built-in defaults
175
210
 
176
- const JobFulfilledStatus = t.enumType(['succeeded', 'failed', 'cancelled']);
211
+ const JobStatus = t.enumType(['started', 'scheduled', 'succeeded', 'failed']); // default: "started"
212
+
213
+ const JobFulfilledStatus = t.enumType(['succeeded', 'failed', 'cancelled']); // default: "succeeded"
177
214
 
178
215
  // Intersection types require explicit defaults
179
216
  const ReportStatus = t.intersection(
180
217
  [JobStatus, JobFulfilledStatus],
181
- t.enumType(['success', 'failed']), // must provide combined default
218
+ t.enumType(['succeeded', 'failed']), // must provide combined default
182
219
  );
183
220
  ```
184
221
 
185
222
  This is because intersection types can be created from arbitrary types, making it impossible to automatically determine appropriate default values. However, when all constituent types are record types, you can use the `mergeRecords` function to avoid specifying defaults:
186
223
 
187
- ```typescript
224
+ ```tsx
225
+ import * as t from 'ts-fortress';
226
+
188
227
  // Using mergeRecords for record-only intersections
189
- const UserWithMetadata = t.mergeRecords(
228
+ const UserWithMetadata = t.mergeRecords([
190
229
  t.record({
191
230
  id: t.string(),
192
231
  name: t.string(),
@@ -195,441 +234,58 @@ const UserWithMetadata = t.mergeRecords(
195
234
  createdAt: t.number(),
196
235
  updatedAt: t.number(),
197
236
  }),
198
- );
199
- // No explicit default needed - automatically combines defaults from both records
200
- // Default: { id: '', name: '', createdAt: 0, updatedAt: 0 }
237
+ // No explicit default needed - automatically combines defaults from both records
238
+ ]);
239
+
240
+ assert.deepStrictEqual(UserWithMetadata.defaultValue, {
241
+ id: '',
242
+ name: '',
243
+ createdAt: 0,
244
+ updatedAt: 0,
245
+ } satisfies t.TypeOf<typeof UserWithMetadata>);
201
246
  ```
202
247
 
203
248
  ## Why ts-fortress over Zod and io-ts?
204
249
 
205
250
  While ts-fortress, [Zod](https://github.com/colinhacks/zod), and [io-ts](https://github.com/gcanti/io-ts) are all excellent TypeScript validation libraries, ts-fortress offers more readable and informative error messages than both, a more type-safe way of building validators than Zod, and addresses some critical runtime consistency issues found in io-ts.
206
251
 
207
- ### Type Safety when Building Schemas
208
-
209
- **Problem with Zod**: The following code compiles without errors but creates an invalid schema:
210
-
211
- ```typescript
212
- import * as z from 'zod';
213
-
214
- // ❌ This compiles but is incorrect!
215
- export const SomeObject = z.object({
216
- key1: 1, // Should be z.literal(1)
217
- key2: 'string', // Should be z.string()
218
- });
219
-
220
- export type SomeObject = z.infer<typeof SomeObject>; // inferred as { key1: unknown, key2: unknown }
221
- ```
222
-
223
- The above Zod schema will fail at runtime because raw values (`1`, `'string'`) are not valid Zod validators.
224
-
225
- **Correct Zod usage** requires remembering to wrap all values:
226
-
227
- ```typescript
228
- // ✅ Correct Zod usage
229
- export const SomeObject = z.object({
230
- key1: z.literal(1),
231
- key2: z.string(),
232
- });
233
- ```
234
-
235
- **ts-fortress prevents this error at compile time**:
236
-
237
- ```typescript
238
- import * as t from 'ts-fortress';
239
-
240
- // ❌ TypeScript error - this won't compile!
241
- export const SomeObject = t.record({
242
- key1: 1, // Error: number is not assignable to Type<unknown>
243
- key2: 'string', // Error: string is not assignable to Type<unknown>
244
- });
245
-
246
- // ✅ Correct ts-fortress usage - enforced by TypeScript
247
- export const SomeObject = t.record({
248
- key1: t.literal(1), // or t.number(1) with default
249
- key2: t.string(''),
250
- });
251
- ```
252
-
253
- ### Benefits
254
-
255
- - **Compile-time safety**: TypeScript catches invalid schema definitions immediately
256
- - **IDE support**: Better autocomplete and error highlighting during development
257
- - **Reduced runtime errors**: Impossible to create invalid schemas that fail at runtime
258
- - **Self-documenting**: The type system guides you toward correct usage
259
-
260
- ### Deep Readonly Types by Default
261
-
262
- **ts-fortress generates deeply readonly types**, promoting immutability and preventing accidental mutations:
263
-
264
- ```typescript
265
- import * as t from 'ts-fortress';
266
-
267
- const UserType = t.record({
268
- name: t.string(''),
269
- address: t.record({
270
- street: t.string(''),
271
- city: t.string(''),
272
- }),
273
- tags: t.array(t.string('')),
274
- });
275
-
276
- type User = t.TypeOf<typeof UserType>;
277
- // ↑ Readonly<{
278
- // name: string;
279
- // address: Readonly<{
280
- // street: string;
281
- // city: string;
282
- // }>;
283
- // tags: readonly string[];
284
- // }>
285
-
286
- const user: User = UserType.cast(someData);
287
-
288
- // ❌ All of these produce TypeScript errors:
289
- user.name = 'new name'; // Cannot assign to 'name' because it is read-only
290
- user.address.street = 'new street'; // Cannot assign to 'street' because it is read-only
291
- user.tags.push('new tag'); // Property 'push' does not exist on readonly array
292
- user.tags[0] = 'modified'; // Index signature in type 'readonly string[]' only permits reading
293
- ```
294
-
295
- **In contrast, Zod types are mutable by default**:
296
-
297
- ```typescript
298
- import * as z from 'zod';
299
-
300
- const UserSchema = z.object({
301
- name: z.string(),
302
- address: z.object({
303
- street: z.string(),
304
- city: z.string(),
305
- }),
306
- tags: z.array(z.string()),
307
- });
308
-
309
- type User = z.infer<typeof UserSchema>;
310
- // ↑ {
311
- // name: string;
312
- // address: {
313
- // street: string;
314
- // city: string;
315
- // };
316
- // tags: string[];
317
- // }
318
-
319
- const user: User = UserSchema.parse(someData);
320
-
321
- // ✅ These all work in Zod (but may not be desirable):
322
- user.name = 'new name'; // No error
323
- user.address.street = 'new street'; // No error
324
- user.tags.push('new tag'); // No error
325
- user.tags[0] = 'modified'; // No error
326
- ```
327
-
328
- **In io-ts, Readonly types will generate verbose error messages**:
329
-
330
- ```typescript
331
- import * as ioTs from 'io-ts';
332
- import { PathReporter } from 'io-ts/PathReporter';
333
-
334
- // io-ts nested readonly version with multiple Readonly wrappers
335
- const IoTsNestedReadonly = ioTs.readonly(
336
- ioTs.type({
337
- user: ioTs.readonly(
338
- ioTs.type({
339
- profile: ioTs.readonly(
340
- ioTs.type({
341
- age: ioTs.number,
342
- }),
343
- ),
344
- }),
345
- ),
346
- }),
347
- );
348
-
349
- const invalidData = {
350
- user: {
351
- profile: {
352
- age: 'not-a-number', // should be number
353
- },
354
- },
355
- };
356
-
357
- // Get io-ts error messages
358
- const ioTsResult = IoTsNestedReadonly.decode(invalidData);
359
- const ioTsErrorMessages = PathReporter.report(ioTsResult);
360
-
361
- console.log(ioTsErrorMessages[0]);
362
- // Output: Invalid value "not-a-number" supplied to : Readonly<{ user: Readonly<{ profile: Readonly<{ age: number }> }> }>/user: Readonly<{ profile: Readonly<{ age: number }> }>/profile: Readonly<{ age: number }>/age: number
363
- ```
364
-
365
- **The error message contains excessive `Readonly<...>` wrapper noise:**
366
-
367
- - Root level: `Readonly<{ user: Readonly<{ profile: Readonly<{ age: number }> }> }>`
368
- - User level: `Readonly<{ profile: Readonly<{ age: number }> }>`
369
- - Profile level: `Readonly<{ age: number }>`
370
- - Each nested readonly creates exponentially verbose error messages with redundant type information
371
-
372
- This verbosity makes error messages extremely difficult to read and debug in production environments.
373
-
374
- **ts-fortress addresses this issue** by generating clean, actionable error messages without verbose type wrapper details:
375
-
376
- ```typescript
377
- import * as tf from 'ts-fortress';
378
-
379
- // ts-fortress equivalent clean structure
380
- const TsFortressNestedType = tf.record({
381
- user: tf.record({
382
- profile: tf.record({
383
- age: tf.number(0),
384
- }),
385
- }),
386
- });
387
-
388
- // Get ts-fortress error messages
389
- const tsFortressResult = TsFortressNestedType.validate(invalidData);
390
- const tsFortressErrorMessages = tf.Result.isErr(tsFortressResult)
391
- ? tf.validationErrorsToMessages(tsFortressResult.value)
392
- : [];
393
-
394
- console.log(tsFortressErrorMessages[0]);
395
- // Clean output: Expected <number> at user.profile.age, got <string> type value "not-a-number".
396
- ```
397
-
398
- **ts-fortress vs Zod comparison:**
399
-
400
- ```typescript
401
- import { z } from 'zod';
402
-
403
- // Zod nested readonly equivalent
404
- const ZodNestedType = z
405
- .object({
406
- user: z
407
- .object({
408
- profile: z
409
- .object({
410
- age: z.number(),
411
- })
412
- .readonly(),
413
- })
414
- .readonly(),
415
- })
416
- .readonly();
417
-
418
- // Get Zod error messages using prettifyError
419
- const zodResult = ZodNestedType.safeParse(invalidData);
420
- const zodErrorMessages = zodResult.success
421
- ? ''
422
- : z.prettifyError(zodResult.error);
423
-
424
- console.log(zodErrorMessages);
425
- // Clean output: "✖ Invalid input: expected number, received string\n → at user.profile.age"
426
- ```
427
-
428
- **Error message comparison:**
429
-
430
- - **io-ts**: 148 characters of verbose type information mixed with the actual error
431
- - **Zod**: 62 characters with visual formatting but missing the actual invalid value
432
- - **ts-fortress**: 74 characters focused purely on what went wrong and where, including the actual invalid value
433
-
434
- While Zod produces cleaner error messages than io-ts and includes helpful visual formatting, **ts-fortress provides superior debugging experience** by including the actual invalid value (`"not-a-number"`) in the error message, making it easier to understand what data caused the validation failure.
435
-
436
- ### Benefits of Deep Readonly
437
-
438
- - **Immutability by default**: Prevents accidental mutations that can lead to bugs
439
- - **Functional programming support**: Encourages functional programming patterns
440
- - **Predictable data flow**: Ensures data integrity throughout your application
441
- - **Thread safety**: Immutable data is inherently safe to share between contexts
442
-
443
- ### Runtime-Type Consistency Issues in io-ts
444
-
445
- **io-ts has several long-standing bugs** where runtime behavior doesn't match TypeScript types, which have remained unfixed for years:
446
-
447
- #### 1. Keyof Type Mismatch ([Issue #697](https://github.com/gcanti/io-ts/issues/697))
448
-
449
- ```typescript
450
- import * as t from 'io-ts';
451
-
452
- const T = t.keyof({
453
- 0: undefined,
454
- 1: undefined,
455
- 2: undefined,
456
- 3: undefined,
457
- 4: undefined,
458
- });
459
-
460
- // ❌ Runtime behavior is inconsistent with TypeScript types!
461
- console.log(T.decode(0)); // Left (fails) - number 0 is rejected
462
- console.log(T.decode('0')); // Right (succeeds) - string "0" is accepted
463
-
464
- type T = t.TypeOf<typeof T>;
465
- // ↑ TypeScript infers: 0 | 1 | 2 | 3 | 4 (number literals)
466
- // But should be: "0" | "1" | "2" | "3" | "4" (string literals)
467
-
468
- // The runtime validator only accepts strings, but TypeScript thinks it accepts numbers!
469
- ```
470
-
471
- For this reason, the [io-ts documentation](https://github.com/gcanti/io-ts/blob/master/index.md#implemented-types--combinators) states that `t.keyof` only supports string keys (although this is somewhat dangerous, as the TypeScript type definition does not prevent the use of keys other than string).
472
-
473
- #### 2. Union + Undefined Decode Issues ([Issue #677](https://github.com/gcanti/io-ts/issues/677))
474
-
475
- ```typescript
476
- import * as t from 'io-ts';
477
- import { isRight } from 'fp-ts/Either';
478
-
479
- const A = t.type({
480
- A: t.union([t.number, t.undefined, t.null]),
481
- });
482
-
483
- const B = t.type({
484
- B: t.union([t.number, t.undefined, t.null]),
485
- });
486
-
487
- const C = t.partial({
488
- C: t.union([t.number, t.null]),
489
- });
490
-
491
- // ❌ Case 1: Union decode adds unexpected fields
492
- {
493
- const UnionBA = t.union([B, A]);
494
- const res = UnionBA.decode({ A: 1 });
495
-
496
- if (isRight(res)) {
497
- console.log(res.right); // { A: 1, B: undefined } <- NG (expected: { A: 1 })
498
- console.log(A.is(res.right)); // true <- ok
499
- console.log(B.is(res.right)); // true <- NG (expected: false)
500
- }
501
- }
502
-
503
- // ❌ Case 2: Union decode produces inconsistent results
504
- {
505
- const UnionCA = t.union([C, A]);
506
- const res = UnionCA.decode({ A: 1 });
507
-
508
- if (isRight(res)) {
509
- console.log(res.right); // { A: 1 } <- NG (expected: {})
510
- console.log(A.is(res.right)); // true <- ok
511
- console.log(C.is(res.right)); // true <- ok
512
- }
513
- }
514
- ```
515
-
516
- **ts-fortress eliminates these problems** by ensuring strict runtime-type consistency:
517
-
518
- Keyof Type Mismatch:
519
-
520
- ```typescript
521
- import * as t from 'ts-fortress';
522
-
523
- // ✅ ts-fortress: Runtime and types always match
524
- const T = t.keyof(
525
- t.record({
526
- 0: t.undefinedType,
527
- 1: t.undefinedType,
528
- 2: t.undefinedType,
529
- 3: t.undefinedType,
530
- 4: t.undefinedType,
531
- }),
532
- );
533
-
534
- type T = t.TypeOf<typeof T>;
535
- // ↑ TypeScript correctly infers: "0" | "1" | "2" | "3" | "4" (string literals)
536
-
537
- // ✅ Runtime behavior matches TypeScript types exactly
538
- console.log(T.validate(0)); // ❌ Fails correctly - number 0 is rejected
539
- console.log(T.validate('0')); // ✅ Success - string "0" is accepted
540
-
541
- // For this use case, if you want to define a union type of numeric literals, you can use `uintRange` from ts-fortress:
542
-
543
- const U = t.uintRange({ start: 0, end: 5 });
544
-
545
- type U = t.TypeOf<typeof U>;
546
- // ↑ TypeScript correctly infers: 0 | 1 | 2 | 3 | 4 (number literals)
547
-
548
- console.log(U.validate('0')); // ❌ Fails - string "0" is rejected
549
- console.log(U.validate(0)); // ✅ Success - number 0 is accepted
550
- ```
551
-
552
- Union + Undefined Decode Issues:
553
-
554
- ```ts
555
- import * as t from 'ts-fortress';
556
-
557
- // ✅ Complex union types work reliably without unexpected behavior
558
- const A = t.record({
559
- A: t.union([t.number(0), t.undefinedType, t.nullType]),
560
- });
561
-
562
- const B = t.record({
563
- B: t.union([t.number(0), t.undefinedType, t.nullType]),
564
- });
565
-
566
- const C = t.partial(
567
- t.record({
568
- C: t.union([t.number(0), t.nullType]),
569
- }),
570
- );
571
-
572
- // ✅ Case 1: Union validation is predictable and correct
573
- {
574
- const UnionBA = t.union([B, A]);
575
- const result = UnionBA.validate({ A: 1 });
576
-
577
- if (t.Result.isOk(result)) {
578
- console.log(result.value); // { A: 1 } <- ✅ Correct! No unexpected fields
579
- console.log(A.is(result.value)); // true <- ✅ Correct
580
- console.log(B.is(result.value)); // false <- ✅ Correct! B requires field B
581
- }
582
- }
583
-
584
- // ✅ Case 2: Consistent validation behavior
585
- {
586
- const UnionCA = t.union([C, A]);
587
- const result = UnionCA.validate({ A: 1 });
588
-
589
- if (t.Result.isOk(result)) {
590
- console.log(result.value); // { A: 1 } <- ✅ Correct and consistent
591
- console.log(A.is(result.value)); // true <- ✅ Correct
592
- console.log(C.is(result.value)); // true <- ✅ Consistent! ts-fortress partial types allow extra fields
593
- }
594
- }
595
- ```
596
-
597
- This makes ts-fortress especially valuable in large codebases where schema correctness is critical and runtime failures need to be minimized.
252
+ For more information, please see [this documentation](./documents/why-ts-fortress-over-zod-and-io-ts.md).
598
253
 
599
254
  ### Migration from io-ts
600
255
 
601
256
  If you're coming from io-ts, here's how common patterns translate:
602
257
 
603
- ```typescript
258
+ ```tsx
604
259
  // io-ts style
605
260
  import * as t from 'io-ts';
606
261
 
607
- const UserCodec = t.type({
262
+ const User = t.type({
608
263
  id: t.string,
609
264
  name: t.string,
610
265
  age: t.number,
611
266
  });
612
267
 
613
- type User = t.TypeOf<typeof UserCodec>;
268
+ type User = t.TypeOf<typeof User>;
269
+ ```
614
270
 
271
+ ```tsx
615
272
  // ts-fortress style
616
273
  import * as t from 'ts-fortress';
617
274
 
618
- const UserType = t.record({
619
- id: t.string(''),
620
- name: t.string(''),
621
- age: t.number(0),
275
+ const User = t.record({
276
+ id: t.string(),
277
+ name: t.string(),
278
+ age: t.number(20),
622
279
  });
623
280
 
624
- type User = t.TypeOf<typeof UserType>;
281
+ type User = t.TypeOf<typeof User>;
625
282
  ```
626
283
 
627
284
  Key differences:
628
285
 
629
- - **Default values**: ts-fortress requires explicit default values for better type safety
286
+ - **Default values**: ts-fortress types are functions to allow for explicit default values ​​etc.
630
287
  - **Naming**: `record` instead of `type`, more explicit function names
631
288
  - **Error handling**: `Result` type instead of `Either`
632
- - **Branded types**: Rich collection of pre-built branded number types
633
289
 
634
290
  ## Core Concepts
635
291
 
@@ -637,7 +293,7 @@ Key differences:
637
293
 
638
294
  Every validator in ts-fortress implements the `Type<A>` interface:
639
295
 
640
- ```typescript
296
+ ```tsx
641
297
  type Type<A> = Readonly<{
642
298
  typeName: string; // Human-readable type name
643
299
  defaultValue: A; // Default value for this type
@@ -655,69 +311,79 @@ type Type<A> = Readonly<{
655
311
 
656
312
  The `validate` method performs comprehensive validation and returns a `Result` type. **When validation succeeds, it returns the original input object (same reference)**, preserving object identity:
657
313
 
658
- ```typescript
314
+ ```tsx
659
315
  import * as t from 'ts-fortress';
660
316
 
661
317
  const User = t.record({
662
- name: t.string(''),
663
- age: t.number(0),
318
+ name: t.string(),
319
+ age: t.number(),
664
320
  });
665
321
 
666
322
  // Success case - returns the same object reference
667
- const validData = { name: 'Alice', age: 30 };
323
+ const validData = { name: 'Alice', age: 30 } as const;
668
324
  const result = User.validate(validData);
669
325
 
670
- if (t.Result.isOk(result)) {
671
- console.log(result.value === validData); // true - same reference!
672
- console.log(result.value); // { name: 'Alice', age: 30 }
673
- }
326
+ assert(t.Result.isOk(result));
327
+ assert(result.value === validData); // true - same reference!
328
+
329
+ assert.deepStrictEqual(result.value, { name: 'Alice', age: 30 });
674
330
 
675
331
  // Error case - provides detailed error information
676
- const invalidData = { name: 'Bob', age: 'thirty' };
332
+ const invalidData = { name: 'Bob', age: 'thirty' } as const;
677
333
  const errorResult = User.validate(invalidData);
678
334
 
679
- if (t.Result.isErr(errorResult)) {
680
- console.log(errorResult.value);
681
- // [
682
- // {
683
- // path: ['age'],
684
- // actualValue: 'thirty',
685
- // expectedType: 'number',
686
- // typeName: 'number'
687
- // }
688
- // ]
689
- }
335
+ assert(t.Result.isErr(errorResult));
336
+
337
+ assert.deepStrictEqual(errorResult.value, [
338
+ {
339
+ path: ['age'],
340
+ actualValue: 'thirty',
341
+ expectedType: 'number',
342
+ typeName: 'number',
343
+ message: undefined,
344
+ },
345
+ ]);
346
+
347
+ assert.deepStrictEqual(t.validationErrorsToMessages(errorResult.value), [
348
+ 'Expected <number> at age, got <string> type value "thirty".',
349
+ ]);
690
350
  ```
691
351
 
692
352
  ##### `assertIs` - Type assertion with runtime checking
693
353
 
694
354
  When using `assertIs`, you must assign it to a typed variable with an explicit type annotation due to TypeScript's limitations with assertion functions:
695
355
 
696
- ```typescript
356
+ ```tsx
697
357
  import * as t from 'ts-fortress';
698
358
 
699
- const numberType = t.number(0);
359
+ const numberType = t.number();
700
360
 
701
361
  // ✅ Correct usage - explicit type annotation required
702
362
  const assertIsNumber: (a: unknown) => asserts a is number = numberType.assertIs;
703
363
 
704
- function processValue(value: unknown) {
705
- // After assertion, TypeScript knows value is a number
364
+ const processValue = (value: unknown): void => {
706
365
  assertIsNumber(value);
707
- console.log(value.toFixed(2)); // TypeScript knows this is safe
708
- }
366
+
367
+ // After assertion, TypeScript knows value is a number
368
+ assertType<number>(value);
369
+ };
709
370
 
710
371
  try {
711
372
  processValue(42); // Works
712
373
  processValue('not a number'); // Throws error
713
374
  } catch (error) {
714
- console.error('Assertion failed:', error);
375
+ assert.deepStrictEqual(
376
+ error,
377
+ new Error(
378
+ `\nExpected <number>, got <string> type value "not a number".`,
379
+ ),
380
+ );
715
381
  }
716
382
 
717
383
  // Example with complex types
718
384
  const User = t.record({
719
- id: t.string(''),
720
- name: t.string(''),
385
+ id: t.string(),
386
+ name: t.string(),
721
387
  });
722
388
 
723
389
  type User = t.TypeOf<typeof User>;
@@ -725,34 +391,38 @@ type User = t.TypeOf<typeof User>;
725
391
  // Explicit type annotation for the assertion function
726
392
  const assertIsUser: (a: unknown) => asserts a is User = User.assertIs;
727
393
 
728
- function processUser(data: unknown) {
394
+ const processUser = (data: unknown): void => {
729
395
  assertIsUser(data);
396
+
730
397
  // TypeScript now knows data is User type
731
- console.log(`User ${data.name} has ID ${data.id}`);
732
- }
398
+ assertType<User>(data);
399
+ };
733
400
  ```
734
401
 
735
402
  ##### `cast` - Type casting with validation
736
403
 
737
404
  The `cast` method validates the input and returns it if valid, otherwise **throws an Error** with validation details:
738
405
 
739
- ```typescript
406
+ ```tsx
740
407
  import * as t from 'ts-fortress';
741
408
 
742
409
  const Port = t.number(8080);
743
410
 
744
- console.log(Port.cast(3000)); // 3000 - valid number
411
+ assert(Port.cast(3000) === 3000); // 3000 is a valid number
745
412
 
746
413
  try {
747
- console.log(Port.cast('invalid')); // Throws Error!
414
+ Port.cast('invalid'); // Throws Error!
748
415
  } catch (error) {
749
- console.error(error.message); // "Expected number, got string"
416
+ assert.deepStrictEqual(
417
+ error,
418
+ new Error('Expected <number>, got <string> type value "invalid".'),
419
+ );
750
420
  }
751
421
  ```
752
422
 
753
423
  ##### `fill` - Intelligent default value filling
754
424
 
755
- The `fill` method attempts to preserve valid parts of the input while filling in missing or invalid values with defaults:
425
+ The `fill` method attempts to preserve valid parts of the input while filling in missing or invalid values with defaults.
756
426
 
757
427
  See [Default Values and Data Filling](#default-values-and-data-filling)
758
428
 
@@ -760,37 +430,40 @@ See [Default Values and Data Filling](#default-values-and-data-filling)
760
430
 
761
431
  Every type has a `defaultValue` property that can be used for initialization:
762
432
 
763
- ```typescript
433
+ ```tsx
764
434
  import * as t from 'ts-fortress';
765
435
 
766
436
  const User = t.record({
767
- id: t.string(''),
437
+ id: t.string(),
768
438
  name: t.string('Guest'),
769
- score: t.number(0),
439
+ score: t.number(),
770
440
  });
771
441
 
772
442
  type User = t.TypeOf<typeof User>;
773
443
 
774
444
  // Use defaultValue for initialization
775
445
  const newUser: User = { ...User.defaultValue, id: 'user-123' };
776
- console.log(newUser);
777
- // { id: 'user-123', name: 'Guest', score: 0 }
446
+ // This default value filling process can also be written as follows:
447
+ const newUser2: User = User.fill({ id: 'user-456' });
448
+
449
+ assert.deepStrictEqual(newUser, { id: 'user-123', name: 'Guest', score: 0 });
450
+ assert.deepStrictEqual(newUser2, { id: 'user-456', name: 'Guest', score: 0 });
778
451
 
779
452
  // Useful for React state initialization
780
- function UserForm() {
453
+ const UserForm = () => {
781
454
  const [formData, setFormData] = useState<User>(User.defaultValue);
782
455
  // ...
783
- }
456
+ };
784
457
  ```
785
458
 
786
459
  ### Primitive Types
787
460
 
788
- ```typescript
461
+ ```tsx
789
462
  import * as t from 'ts-fortress';
790
463
 
791
464
  // Basic primitives
792
465
  const stringType = t.string('default');
793
- const numberType = t.number(0);
466
+ const numberType = t.number();
794
467
  const booleanType = t.boolean(false);
795
468
  const nullType = t.nullType;
796
469
  const undefinedType = t.undefinedType;
@@ -800,36 +473,36 @@ const statusType = t.literal('active');
800
473
  const versionType = t.literal(1);
801
474
 
802
475
  // Arrays
803
- const stringArrayType = t.array(t.string(''));
804
- const nonEmptyArrayType = t.nonEmptyArray(t.number(0));
476
+ const stringArrayType = t.array(t.string());
477
+ const nonEmptyArrayType = t.nonEmptyArray(t.number());
805
478
 
806
479
  // Tuples
807
- const coordinateType = t.tuple([t.number(0), t.number(0)]);
480
+ const coordinateType = t.tuple([t.number(), t.number()]);
808
481
  ```
809
482
 
810
483
  ### Record Types
811
484
 
812
- ```typescript
485
+ ```tsx
813
486
  import * as t from 'ts-fortress';
814
487
 
815
488
  // Define object schemas
816
- const PersonType = t.record({
817
- firstName: t.string(''),
818
- lastName: t.string(''),
819
- age: t.number(0),
489
+ const Person = t.record({
490
+ firstName: t.string(),
491
+ lastName: t.string(),
492
+ age: t.number(),
820
493
  address: t.record({
821
- street: t.string(''),
822
- city: t.string(''),
823
- zipCode: t.string(''),
494
+ street: t.string(),
495
+ city: t.string(),
496
+ zipCode: t.string(),
824
497
  }),
825
498
  });
826
499
 
827
- type Person = t.TypeOf<typeof PersonType>;
500
+ type Person = t.TypeOf<typeof Person>;
828
501
 
829
502
  // Optional fields
830
- const UserProfileType = t.record({
831
- username: t.string(''),
832
- bio: t.optional(t.string('')), // Optional field
503
+ const UserProfile = t.record({
504
+ username: t.string(),
505
+ bio: t.optional(t.string()), // Optional field
833
506
  settings: t.partial(
834
507
  t.record({
835
508
  // Partial record (all fields optional)
@@ -842,8 +515,8 @@ const UserProfileType = t.record({
842
515
  // Strict validation (disallow excess properties)
843
516
  const StrictUserType = t.record(
844
517
  {
845
- id: t.string(''),
846
- name: t.string(''),
518
+ id: t.string(),
519
+ name: t.string(),
847
520
  },
848
521
  {
849
522
  allowExcessProperties: false, // Reject any properties not defined in schema
@@ -852,15 +525,15 @@ const StrictUserType = t.record(
852
525
 
853
526
  // Alternatively, use the strictRecord alias for cleaner syntax
854
527
  const StrictUserTypeAlias = t.strictRecord({
855
- id: t.string(''),
856
- name: t.string(''),
528
+ id: t.string(),
529
+ name: t.string(),
857
530
  });
858
531
 
859
532
  // Permissive validation (allow excess properties) - this is the default
860
533
  const PermissiveUserType = t.record(
861
534
  {
862
- id: t.string(''),
863
- name: t.string(''),
535
+ id: t.string(),
536
+ name: t.string(),
864
537
  },
865
538
  {
866
539
  allowExcessProperties: true, // Allow additional properties (default behavior)
@@ -869,17 +542,17 @@ const PermissiveUserType = t.record(
869
542
 
870
543
  // Example usage - both StrictUserType and StrictUserTypeAlias behave identically
871
544
  const strictData = { id: '123', name: 'John', extra: 'not allowed' };
872
- console.log(StrictUserType.is(strictData)); // false - 'extra' property causes rejection
873
- console.log(StrictUserTypeAlias.is(strictData)); // false - same as above
545
+ assert(!StrictUserType.is(strictData)); // 'extra' property causes rejection
546
+ assert(!StrictUserTypeAlias.is(strictData)); // same as above
874
547
 
875
548
  const permissiveData = { id: '123', name: 'John', extra: 'allowed' };
876
- console.log(PermissiveUserType.is(permissiveData)); // true - 'extra' property is allowed
549
+ assert(PermissiveUserType.is(permissiveData)); // 'extra' property is allowed
877
550
 
878
551
  // strictRecord provides cleaner syntax for strict validation
879
552
  const UserSchema = t.strictRecord({
880
- name: t.string(''),
881
- email: t.string(''),
882
- age: t.number(0),
553
+ name: t.string(),
554
+ email: t.string(),
555
+ age: t.number(),
883
556
  });
884
557
 
885
558
  // Validation examples
@@ -892,27 +565,173 @@ UserSchema.is({
892
565
  }); // ❌ false - excess property
893
566
  ```
894
567
 
568
+ ### Refined Types
569
+
570
+ ts-fortress provides the `refine` function to create refined types with custom validation logic while leveraging existing base types:
571
+
572
+ ```tsx
573
+ import * as t from 'ts-fortress';
574
+
575
+ // Create refined types
576
+ const Uuid = t.refine({
577
+ baseType: t.string(),
578
+ // Define custom validation logic
579
+ is: (value: string): value is string =>
580
+ /^[\da-f]{8}-[\da-f]{4}-[0-5][\da-f]{3}-[089ab][\da-f]{3}-[\da-f]{12}$/iu.test(
581
+ value,
582
+ ),
583
+ defaultValue: '00000000-1111-2222-3333-444444444444',
584
+ typeName: 'Uuid',
585
+ });
586
+
587
+ type Uuid = t.TypeOf<typeof Uuid>; // string (with runtime validation)
588
+
589
+ const PositiveNumber = t.refine({
590
+ baseType: t.number(1),
591
+ is: (value: number): value is number => value > 0,
592
+ defaultValue: 1,
593
+ typeName: 'PositiveNumber',
594
+ });
595
+
596
+ type PositiveNumber = t.TypeOf<typeof PositiveNumber>; // number (with runtime validation)
597
+
598
+ const EvenNumber = t.refine({
599
+ baseType: t.number(),
600
+ is: (value: number): value is number => value % 2 === 0,
601
+ defaultValue: 0,
602
+ typeName: 'EvenNumber',
603
+ });
604
+
605
+ // Usage in validation
606
+ const uuidResult = Uuid.validate('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b');
607
+
608
+ assert(t.Result.isOk(uuidResult));
609
+
610
+ if (t.Result.isOk(uuidResult)) {
611
+ const validUuid = uuidResult.value; // string, guaranteed to be valid Uuid format
612
+ }
613
+
614
+ const positiveResult = PositiveNumber.validate(42);
615
+
616
+ assert(t.Result.isOk(positiveResult));
617
+
618
+ if (t.Result.isOk(positiveResult)) {
619
+ const positiveNum = positiveResult.value; // number, guaranteed to be > 0
620
+ }
621
+
622
+ // Invalid cases
623
+ assert(!Uuid.is('invalid-uuid'));
624
+ assert(!PositiveNumber.is(-5));
625
+ assert(!EvenNumber.is(7));
626
+
627
+ // Use in record schemas
628
+ const UserProfile = t.record({
629
+ id: Uuid, // refined uuid validation
630
+ score: PositiveNumber, // must be positive
631
+ level: EvenNumber, // must be even
632
+ });
633
+
634
+ type UserProfile = t.TypeOf<typeof UserProfile>;
635
+
636
+ // The refined types maintain their validation in composite types
637
+ const userData = {
638
+ id: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b', // ✅ valid uuid format
639
+ score: 85, // ✅ positive number
640
+ level: 4, // ✅ even number
641
+ } as const satisfies UserProfile;
642
+
643
+ assert(UserProfile.is(userData));
644
+
645
+ const invalidData = {
646
+ id: 'user123', // ❌ invalid uuid format
647
+ score: -10, // ❌ negative number
648
+ level: 3, // ❌ odd number
649
+ } as const;
650
+
651
+ const result = UserProfile.validate(invalidData);
652
+
653
+ assert(t.Result.isErr(result));
654
+
655
+ assert.deepStrictEqual(
656
+ t.validationErrorsToMessages(
657
+ result.value satisfies readonly t.ValidationError[],
658
+ ),
659
+ [
660
+ 'Expected <Uuid> at id, got <string> type value "user123".',
661
+ 'Expected <PositiveNumber> at score, got <number> type value `-10`.',
662
+ 'Expected <EvenNumber> at level, got <number> type value `3`.',
663
+ ],
664
+ );
665
+ ```
666
+
667
+ #### Key Benefits of Refined Types
668
+
669
+ - **Composable validation**: Build on existing base types while adding custom constraints
670
+ - **Type safety**: TypeScript types reflect the refined constraints at compile time
671
+ - **Clear error messages**: Validation errors clearly indicate which refinement failed
672
+ - **Reusable logic**: Define validation logic once and reuse across multiple schemas
673
+ - **Performance**: Leverages base type validation before applying custom refinements
674
+
675
+ #### Common Use Cases
676
+
677
+ ```tsx
678
+ import * as t from 'ts-fortress';
679
+
680
+ // Domain-specific string types
681
+ const PhoneNumber = t.refine({
682
+ baseType: t.string(),
683
+ is: (s): s is string => /^\+?[\d\s()-]+$/u.test(s),
684
+ defaultValue: '+1234567890',
685
+ typeName: 'PhoneNumber',
686
+ });
687
+
688
+ const ZipCode = t.refine({
689
+ baseType: t.string(),
690
+ is: (s): s is string => /^\d{5}(-\d{4})?$/u.test(s),
691
+ defaultValue: '12345',
692
+ typeName: 'ZipCode',
693
+ });
694
+
695
+ // Constrained numeric types
696
+ const Percentage = t.refine({
697
+ baseType: t.number(),
698
+ is: (n): n is number => 0 <= n && n <= 100,
699
+ defaultValue: 0,
700
+ typeName: 'Percentage',
701
+ });
702
+
703
+ const Port = t.refine({
704
+ baseType: t.number(3000),
705
+ is: (n): n is number => Number.isInteger(n) && 1 <= n && n <= 65_535,
706
+ defaultValue: 3000,
707
+ typeName: 'Port',
708
+ });
709
+ ```
710
+
895
711
  ### Branded Types
896
712
 
897
713
  ts-fortress provides extensive support for branded types to create domain-specific validation:
898
714
 
899
- ```typescript
715
+ ```tsx
900
716
  import * as t from 'ts-fortress';
901
717
 
902
718
  // Simple branded types
903
- const UserId = t.simpleBrandedString({ typeName: 'UserId', defaultValue: '' });
904
- const Weight = t.simpleBrandedNumber({ typeName: 'Weight', defaultValue: 0 });
719
+ const UserId = t.brandedString({ typeName: 'UserId', defaultValue: '' });
720
+ const Weight = t.brandedNumber({ typeName: 'Weight', defaultValue: 0 });
905
721
 
906
722
  type UserId = t.TypeOf<typeof UserId>; // Brand<string, 'UserId'>
907
723
  type Weight = t.TypeOf<typeof Weight>; // Brand<number, 'Weight'>
908
724
 
909
725
  // Rich number validation types
910
- const PositiveInt = t.positiveInt(1);
911
- const SafeInt = t.safeInt(0);
912
- const UInt16 = t.uint16(0);
726
+ const PositiveInt = t.positiveInt(t.asPositiveInt(1));
727
+ const SafeInt = t.safeInt(t.asSafeInt(0));
728
+ const UInt16 = t.uint16(t.asUint16(0));
913
729
 
914
730
  // Usage
915
731
  const userIdResult = UserId.validate('user_123');
732
+
733
+ assert(t.Result.isOk(userIdResult));
734
+
916
735
  if (t.Result.isOk(userIdResult)) {
917
736
  const id: UserId = userIdResult.value;
918
737
  }
@@ -920,38 +739,46 @@ if (t.Result.isOk(userIdResult)) {
920
739
 
921
740
  ### Union and Intersection Types
922
741
 
923
- ```typescript
742
+ ```tsx
924
743
  import * as t from 'ts-fortress';
925
744
 
926
745
  // Union types
927
- const IdType = t.union([t.string(''), t.number(0)]);
746
+ const IdType = t.union([t.string(), t.number()]);
928
747
 
929
748
  // Intersection types
930
- const TimestampedType = t.intersection([
931
- t.record({ data: t.string('') }),
749
+ const TimestampedType = t.intersection(
750
+ [
751
+ t.record({ data: t.string() }),
752
+ t.record({
753
+ createdAt: t.number(Date.now()),
754
+ updatedAt: t.number(Date.now()),
755
+ }),
756
+ ],
932
757
  t.record({
758
+ data: t.string(),
933
759
  createdAt: t.number(Date.now()),
934
760
  updatedAt: t.number(Date.now()),
935
761
  }),
936
- ]);
762
+ );
937
763
 
938
764
  // Merge records (similar to intersection but more specific)
939
765
  const ExtendedUserType = t.mergeRecords([
940
766
  PersonType,
941
767
  t.record({
942
- id: t.string(''),
943
- email: t.string(''),
768
+ id: t.string(),
769
+ email: t.string(),
944
770
  }),
945
771
  ]);
946
772
  ```
947
773
 
948
774
  ### Enums
949
775
 
950
- ```typescript
776
+ ```tsx
951
777
  import * as t from 'ts-fortress';
952
778
 
953
779
  // String enums
954
780
  const ColorEnum = t.enumType(['red', 'green', 'blue']);
781
+
955
782
  type Color = t.TypeOf<typeof ColorEnum>; // 'red' | 'green' | 'blue'
956
783
 
957
784
  // Numeric ranges
@@ -960,51 +787,77 @@ const DiceRoll = t.uintRange({
960
787
  end: 7,
961
788
  defaultValue: 1,
962
789
  }); // integers from 1 to 6
790
+
963
791
  type DiceRoll = t.TypeOf<typeof DiceRoll>; // 1 | 2 | 3 | 4 | 5 | 6
964
792
  ```
965
793
 
794
+ Tips: It is often better to use `uintRange` instead of `enumType` when possible, because `enumType` stores a Set of the sizes of its members as data, while `uintRange` only stores the range, resulting in smaller memory usage.
795
+
966
796
  ## Error Handling
967
797
 
968
798
  ts-fortress uses `Result<T, readonly ValidationError[]>` for structured error handling with detailed error information:
969
799
 
970
- ```typescript
800
+ ```tsx
971
801
  import * as t from 'ts-fortress';
972
802
 
973
- const UserType = t.record({
974
- name: t.string(''),
975
- age: t.number(0),
803
+ const User = t.record({
804
+ name: t.string(),
805
+ age: t.number(),
976
806
  });
977
807
 
808
+ type User = t.TypeOf<typeof User>;
809
+
978
810
  const invalidData = { name: 123, age: 'not a number' };
979
811
 
980
- const result = UserType.validate(invalidData);
981
- if (t.Result.isErr(result)) {
982
- // result.value is an array of ValidationError objects
983
- for (const error of result.value) {
984
- console.log('Path:', error.path); // ['name'] or ['age']
985
- console.log('Expected:', error.expectedType); // 'string' or 'number'
986
- console.log('Actual:', error.actualValue); // 123 or 'not a number'
987
- console.log('Type:', error.typeName); // type being validated
988
- }
989
-
990
- // Convert to string messages
991
- const messages = t.validationErrorsToMessages(result.value);
992
- console.log(messages);
993
- // ['Expected string at name, got number', 'Expected number at age, got string']
994
- }
812
+ const result = User.validate(invalidData);
813
+
814
+ assert(t.Result.isErr(result));
815
+
816
+ // result.value is an array of ValidationError objects
817
+ assert.deepStrictEqual(result.value, [
818
+ {
819
+ actualValue: 123,
820
+ expectedType: 'string',
821
+ message: undefined,
822
+ path: ['name'],
823
+ typeName: 'string',
824
+ },
825
+ {
826
+ actualValue: 'not a number',
827
+ expectedType: 'number',
828
+ message: undefined,
829
+ path: ['age'],
830
+ typeName: 'number',
831
+ },
832
+ ] satisfies t.ValidationError[]);
833
+
834
+ // Convert to string messages
835
+ const messages = t.validationErrorsToMessages(result.value);
836
+
837
+ assert.deepStrictEqual(messages, [
838
+ 'Expected <string> at name, got <number> type value `123`.',
839
+ 'Expected <number> at age, got <string> type value "not a number".',
840
+ ]);
841
+
842
+ const assertIsUser: (a: unknown) => asserts a is User = User.assertIs;
995
843
 
996
844
  // Using assertions (throws on invalid data)
997
845
  try {
998
- UserType.assertIs(invalidData);
846
+ assertIsUser(invalidData);
999
847
  } catch (error) {
1000
- console.error('Validation failed:', error.message);
848
+ assert.deepStrictEqual(
849
+ error,
850
+ new Error(
851
+ '\nExpected <string> at name, got <number> type value `123`.,\nExpected <number> at age, got <string> type value "not a number".',
852
+ ),
853
+ );
1001
854
  }
1002
855
 
1003
856
  // Excess property validation example
1004
857
  const StrictType = t.record(
1005
858
  {
1006
- name: t.string(''),
1007
- age: t.number(0),
859
+ name: t.string(),
860
+ age: t.number(),
1008
861
  },
1009
862
  {
1010
863
  allowExcessProperties: false,
@@ -1014,23 +867,24 @@ const StrictType = t.record(
1014
867
  const dataWithExcess = { name: 'John', age: 30, extra: 'not allowed' };
1015
868
  const strictResult = StrictType.validate(dataWithExcess);
1016
869
 
1017
- if (t.Result.isErr(strictResult)) {
1018
- console.log(strictResult.value);
1019
- // [{
1020
- // path: ['extra'],
1021
- // actualValue: 'not allowed',
1022
- // expectedType: '{ name: string, age: number }',
1023
- // message: 'Excess property "extra" is not allowed',
1024
- // typeName: '{ name: string, age: number }'
1025
- // }]
1026
- }
870
+ assert(t.Result.isErr(strictResult));
871
+
872
+ assert.deepStrictEqual(strictResult.value, [
873
+ {
874
+ path: ['extra'],
875
+ actualValue: 'not allowed',
876
+ expectedType: '{ name: string, age: number }',
877
+ message: 'Excess property "extra" is not allowed',
878
+ typeName: '{ name: string, age: number }',
879
+ },
880
+ ]);
1027
881
  ```
1028
882
 
1029
883
  ### ValidationError Structure
1030
884
 
1031
885
  Each validation error provides detailed information:
1032
886
 
1033
- ```typescript
887
+ ```tsx
1034
888
  type ValidationError = Readonly<{
1035
889
  path: readonly string[];
1036
890
  actualValue: unknown; // The actual value that failed validation
@@ -1078,8 +932,9 @@ type ValidationError = Readonly<{
1078
932
  ### Refinement
1079
933
 
1080
934
  - `t.refine({ baseType, is, defaultValue })` - Refine `baseType` by `is` function
1081
- - `t.simpleBrandedString({ typeName, defaultValue, is? })` - Simple string branding
1082
- - `t.simpleBrandedNumber({ typeName, defaultValue, is? })` - Simple number branding
935
+ - `t.brand({ baseType, is, defaultValue, brandKeys, brandFalseKeys?, typeName? })` - Refine `baseType` by `is` function with brand typing
936
+ - `t.brandedString({ typeName, defaultValue, is? })` - String branding
937
+ - `t.brandedNumber({ typeName, defaultValue, is? })` - Number branding
1083
938
  - Number types: `t.int()`, `t.safeInt()`, `t.positiveInt()`, `t.uint16()`, etc.
1084
939
 
1085
940
  ### Utilities