ts-fortress 4.1.0 → 4.1.2
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.
- package/README.md +108 -0
- package/dist/record/record.d.mts +2 -2
- package/dist/record/record.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/record/record.mts +1 -1
package/README.md
CHANGED
|
@@ -291,6 +291,114 @@ user.tags.push('new tag'); // No error
|
|
|
291
291
|
user.tags[0] = 'modified'; // No error
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
+
**In io-ts, Readonly types will generate verbose error messages**:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import * as ioTs from 'io-ts';
|
|
298
|
+
import { PathReporter } from 'io-ts/PathReporter';
|
|
299
|
+
|
|
300
|
+
// io-ts nested readonly version with multiple Readonly wrappers
|
|
301
|
+
const IoTsNestedReadonly = ioTs.readonly(
|
|
302
|
+
ioTs.type({
|
|
303
|
+
user: ioTs.readonly(
|
|
304
|
+
ioTs.type({
|
|
305
|
+
profile: ioTs.readonly(
|
|
306
|
+
ioTs.type({
|
|
307
|
+
age: ioTs.number,
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const invalidData = {
|
|
316
|
+
user: {
|
|
317
|
+
profile: {
|
|
318
|
+
age: 'not-a-number', // should be number
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Get io-ts error messages
|
|
324
|
+
const ioTsResult = IoTsNestedReadonly.decode(invalidData);
|
|
325
|
+
const ioTsErrorMessages = PathReporter.report(ioTsResult);
|
|
326
|
+
|
|
327
|
+
console.log(ioTsErrorMessages[0]);
|
|
328
|
+
// 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
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**The error message contains excessive `Readonly<...>` wrapper noise:**
|
|
332
|
+
|
|
333
|
+
- Root level: `Readonly<{ user: Readonly<{ profile: Readonly<{ age: number }> }> }>`
|
|
334
|
+
- User level: `Readonly<{ profile: Readonly<{ age: number }> }>`
|
|
335
|
+
- Profile level: `Readonly<{ age: number }>`
|
|
336
|
+
- Each nested readonly creates exponentially verbose error messages with redundant type information
|
|
337
|
+
|
|
338
|
+
This verbosity makes error messages extremely difficult to read and debug in production environments.
|
|
339
|
+
|
|
340
|
+
**ts-fortress addresses this issue** by generating clean, actionable error messages without verbose type wrapper details:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import * as tf from 'ts-fortress';
|
|
344
|
+
|
|
345
|
+
// ts-fortress equivalent clean structure
|
|
346
|
+
const TsFortressNestedType = tf.record({
|
|
347
|
+
user: tf.record({
|
|
348
|
+
profile: tf.record({
|
|
349
|
+
age: tf.number(0),
|
|
350
|
+
}),
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Get ts-fortress error messages
|
|
355
|
+
const tsFortressResult = TsFortressNestedType.validate(invalidData);
|
|
356
|
+
const tsFortressErrorMessages = tf.Result.isErr(tsFortressResult)
|
|
357
|
+
? tf.validationErrorsToMessages(tsFortressResult.value)
|
|
358
|
+
: [];
|
|
359
|
+
|
|
360
|
+
console.log(tsFortressErrorMessages[0]);
|
|
361
|
+
// Clean output: Expected <number> at user.profile.age, got <string> type value "not-a-number".
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**ts-fortress vs zod comparison:**
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { z } from 'zod';
|
|
368
|
+
|
|
369
|
+
// zod nested readonly equivalent
|
|
370
|
+
const ZodNestedType = z
|
|
371
|
+
.object({
|
|
372
|
+
user: z
|
|
373
|
+
.object({
|
|
374
|
+
profile: z
|
|
375
|
+
.object({
|
|
376
|
+
age: z.number(),
|
|
377
|
+
})
|
|
378
|
+
.readonly(),
|
|
379
|
+
})
|
|
380
|
+
.readonly(),
|
|
381
|
+
})
|
|
382
|
+
.readonly();
|
|
383
|
+
|
|
384
|
+
// Get zod error messages using prettifyError
|
|
385
|
+
const zodResult = ZodNestedType.safeParse(invalidData);
|
|
386
|
+
const zodErrorMessages = zodResult.success
|
|
387
|
+
? ''
|
|
388
|
+
: z.prettifyError(zodResult.error);
|
|
389
|
+
|
|
390
|
+
console.log(zodErrorMessages);
|
|
391
|
+
// Clean output: "✖ Invalid input: expected number, received string\n → at user.profile.age"
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Error message comparison:**
|
|
395
|
+
|
|
396
|
+
- **io-ts**: 148 characters of verbose type information mixed with the actual error
|
|
397
|
+
- **zod**: 62 characters with visual formatting but missing the actual invalid value
|
|
398
|
+
- **ts-fortress**: 74 characters focused purely on what went wrong and where, including the actual invalid value
|
|
399
|
+
|
|
400
|
+
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.
|
|
401
|
+
|
|
294
402
|
### Benefits of Deep Readonly
|
|
295
403
|
|
|
296
404
|
- **Immutability by default**: Prevents accidental mutations that can lead to bugs
|
package/dist/record/record.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RecordType, type
|
|
1
|
+
import { type RecordType, type Type } from '../type.mjs';
|
|
2
2
|
export declare const record: <const R extends ReadonlyRecord<string, Type<unknown>>>(shape: R, options?: Partial<Readonly<{
|
|
3
3
|
typeName: string;
|
|
4
4
|
/** @default true */
|
|
@@ -27,5 +27,5 @@ export declare const record: <const R extends ReadonlyRecord<string, Type<unknow
|
|
|
27
27
|
*/
|
|
28
28
|
export declare const strictRecord: <const R extends ReadonlyRecord<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
|
|
29
29
|
typeName: string;
|
|
30
|
-
}>>) =>
|
|
30
|
+
}>>) => RecordType<R>;
|
|
31
31
|
//# sourceMappingURL=record.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record.d.mts","sourceRoot":"","sources":["../../src/record/record.mts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"record.d.mts","sourceRoot":"","sources":["../../src/record/record.mts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,IAAI,EACV,MAAM,aAAa,CAAC;AAWrB,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC1E,OAAO,CAAC,EACR,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;IAEjB,oBAAoB;IACpB,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC,CACH,KACA,UAAU,CAAC,CAAC,CAyGd,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAErD,QAAQ,CAAC,EACT,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CACH,KACA,UAAU,CAAC,CAAC,CAC+C,CAAC"}
|
package/package.json
CHANGED
package/src/record/record.mts
CHANGED