@@ -383,6 +383,80 @@ export function is(value: unknown, descriptor: TypeDescriptor): boolean {
383383 }
384384}
385385
386+ function describeDescriptor ( descriptor : TypeDescriptor ) : string {
387+ if ( descriptor . kind === 'struct' ) {
388+ const fields = descriptor . fields ?? [ ]
389+ const shape = `{ ${ fields
390+ . map ( ( f ) => `${ f . name } ${ f . optional ? '?' : '' } : ${ describeDescriptor ( f . type ) } ` )
391+ . join ( '; ' ) } }`
392+ return descriptor . name === '__anon' ? shape : `${ descriptor . name } ${ shape } `
393+ }
394+ if ( descriptor . kind === 'array' ) {
395+ return `${ describeDescriptor ( descriptor . element ?? Object_ ) } []`
396+ }
397+ if ( descriptor . kind === 'optional' ) {
398+ return `${ describeDescriptor ( descriptor . inner ?? Object_ ) } ?`
399+ }
400+ return descriptor . name
401+ }
402+
403+ function describeActual ( value : unknown ) : string {
404+ return describeDescriptor ( of ( value ) )
405+ }
406+
407+ function pathLabel ( path : string ) : string {
408+ return path === '$' ? 'value' : path . slice ( 2 )
409+ }
410+
411+ function explainMismatchAt ( value : unknown , descriptor : TypeDescriptor , path : string ) : string | null {
412+ switch ( descriptor . kind ) {
413+ case 'struct' : {
414+ if ( typeof value !== 'object' || value === null || Array . isArray ( value ) ) {
415+ return `${ pathLabel ( path ) } expected ${ describeDescriptor ( descriptor ) } , got ${ describeActual ( value ) } `
416+ }
417+ for ( const field of descriptor . fields ?? [ ] ) {
418+ const record = value as Record < string , unknown >
419+ const present = field . name in record
420+ const fieldPath = `${ path } .${ field . name } `
421+ if ( field . optional ) {
422+ if ( ! present || record [ field . name ] === null || record [ field . name ] === undefined ) continue
423+ } else if ( ! present ) {
424+ return `${ pathLabel ( fieldPath ) } is missing; expected ${ describeDescriptor ( field . type ) } `
425+ }
426+ const nested = explainMismatchAt ( record [ field . name ] , field . type , fieldPath )
427+ if ( nested ) return nested
428+ }
429+ return null
430+ }
431+ case 'array' : {
432+ if ( ! Array . isArray ( value ) ) {
433+ return `${ pathLabel ( path ) } expected ${ describeDescriptor ( descriptor ) } , got ${ describeActual ( value ) } `
434+ }
435+ const element = descriptor . element
436+ if ( ! element ) return null
437+ for ( let i = 0 ; i < value . length ; i ++ ) {
438+ const nested = explainMismatchAt ( value [ i ] , element , `${ path } [${ i } ]` )
439+ if ( nested ) return nested
440+ }
441+ return null
442+ }
443+ case 'optional' :
444+ if ( value === null || value === undefined ) return null
445+ return descriptor . inner ? explainMismatchAt ( value , descriptor . inner , path ) : null
446+ default :
447+ return is ( value , descriptor )
448+ ? null
449+ : `${ pathLabel ( path ) } expected ${ describeDescriptor ( descriptor ) } , got ${ describeActual ( value ) } `
450+ }
451+ }
452+
453+ function formatMismatch ( prefix : 'type.as' | 'type.tryFrom' , value : unknown , descriptor : TypeDescriptor ) : string {
454+ const root = `expected ${ describeDescriptor ( descriptor ) } , got ${ describeActual ( value ) } `
455+ const detail = explainMismatchAt ( value , descriptor , '$' )
456+ if ( ! detail || detail === `value ${ root } ` ) return `${ prefix } : ${ root } `
457+ return `${ prefix } : ${ root } ; ${ detail } `
458+ }
459+
386460/**
387461 * Thrown by `type.as` when the input value (after the optional
388462 * `castFn`) doesn't match the target descriptor. Caller can `try { … }
@@ -436,9 +510,8 @@ export function as<T = unknown>(
436510) : T {
437511 const v = castFn ? castFn ( value ) : value
438512 if ( ! is ( v , descriptor ) ) {
439- const got = of ( v )
440513 throw new TypeMismatchError (
441- ` type.as: expected ${ descriptor . name } , got ${ got . name } ` ,
514+ formatMismatch ( ' type.as' , v , descriptor ) ,
442515 descriptor ,
443516 v
444517 )
@@ -470,11 +543,10 @@ export function tryFrom<T = unknown>(
470543 }
471544 }
472545 if ( ! is ( v , descriptor ) ) {
473- const got = of ( v )
474546 return {
475547 ok : false ,
476548 error : new TypeMismatchError (
477- ` type.tryFrom: expected ${ descriptor . name } , got ${ got . name } ` ,
549+ formatMismatch ( ' type.tryFrom' , v , descriptor ) ,
478550 descriptor ,
479551 v
480552 ) ,
0 commit comments