interface Decode < D > { readonly forceDecode : ( data : unknown ) => D ; } export interface Decoder < D > { readonly forceDecode : Decode < D > [ 'forceDecode' ] ; readonly decode : ( data : unknown ) => ReturnType < Decode < D > [ 'forceDecode' ] > | null ; readonly validate : ( data : unknown ) => ValidationResult < D > ; readonly is : ( data : unknown ) => data is D ; readonly andThen : < T > ( transformer : ( data : D ) => T ) => Decoder < T > ; } export type ValidationResult < D > = { type : 'ok' ; data : D ; } | { type : 'error' ; error : DecoderError ; } ; export type Output < T extends Decoder < any > > = ReturnType < T [ 'forceDecode' ] > ; export const createDecoder = < D > ( decoder ) : Decoder < D > => ( { ... decoder , decode : ( data ) => { try { return decoder . forceDecode ( data ) ; } catch { return null ; } } , validate : ( data ) => { try { return { type : 'ok' , data : decoder . forceDecode ( data ) } ; } catch ( e ) { if ( e instanceof DecoderError ) { return { type : 'error' , error : e } ; } throw e ; } } , is : ( data ) : data is D => { try { decoder . forceDecode ( data ) ; return true ; } catch { return false ; } } , andThen : ( transformer ) => { return createDecoder ( { forceDecode : ( data ) => transformer ( decoder . forceDecode ( data ) ) } ) ; } } ) ; export class DecoderError extends SyntaxError { constructor ( message ? ) { super ( message ) ; this . name = 'DecoderError' ; Object . setPrototypeOf ( this , new . target . prototype ) ; } } const show = ( data ) : string => JSON . stringify ( data , null , 2 ) ; /**
 * Throws if data is null or undefined
 */ export const checkDefined = ( data ) : data is null | undefined => { if ( data == null ) throw new DecoderError ( 'This value is not there' ) ; return false ; } ; /**
 * If the data is null return null
 * else, pass to the decoder where 'checkDefined'
 * fails only when data is undefined
 */ export const nullable = < D > ( decoder ) : Decoder < null | D > => createDecoder ( { forceDecode : ( data ) => data === null ? null : decoder . forceDecode ( data ) } ) ; const primitiveDecoder = < D > ( dataType , condition ) : Decoder < D > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; if ( ! condition ( data ) ) { throw new DecoderError ( `This is not ${ dataType }: ${ show ( data ) }` ) ; } return data ; } } ) ; export const unknown = createDecoder ( { forceDecode : ( data ) => data } ) ; export const string = primitiveDecoder < string > ( 'a string' , ( $ ) : $ is string => typeof $ === 'string' ) ; export const number = primitiveDecoder < number > ( 'a number' , ( $ ) : $ is number => typeof $ === 'number' && Number . isFinite ( $ ) ) ; export const boolean = primitiveDecoder < boolean > ( 'a boolean' , ( $ ) : $ is boolean => typeof $ === 'boolean' ) ; export const literal = < D extends string | number | boolean > ( literal ) : Decoder < D > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; if ( data !== literal ) { throw new DecoderError ( `Data does not match the literal. Expected: '${ literal as string }', actual value: '${ show ( data ) }'` ) ; } return data as D ; } } ) ; export const oneOf = < D extends readonly any [ ] > ( ... decoders ) : Decoder < D [ number ] > => createDecoder ( { forceDecode : ( data ) => { const errors = [ ] ; for ( const decoder of decoders ) { try { return decoder . forceDecode ( data ) ; } catch ( e ) { errors . push ( e . message ?? 'Unknown error' ) ; } } throw new DecoderError ( `None of the decoders worked:\n${ show ( errors ) }` ) ; } } ) ; export const literalUnion = < D extends ReadonlyArray < string | number | boolean > > ( ... decoders ) : Decoder < D [ number ] > => oneOf ( ... decoders . map ( literal ) ) ; export const regex = < D extends string > ( regex ) : Decoder < D > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; if ( ! regex . test ( data as D ) ) { throw new DecoderError ( `Data '${ data as D }' does not satisfy the regex '${ String ( regex ) }'.` ) ; } return data as D ; } } ) ; function checkArrayType ( data ) { if ( ! Array . isArray ( data ) ) throw new DecoderError ( `This is not an array: ${ show ( data ) }` ) ; } export const array = < D > ( decoder ) : Decoder < D [ ] > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; checkArrayType ( data ) ; return data . map ( decoder . forceDecode ) ; } } ) ; export const tuple = < D extends readonly unknown [ ] > ( ... decoders ) : Decoder < D > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; checkArrayType ( data ) ; if ( decoders . length > data . length ) { throw new DecoderError ( `The tuple is not long enough. ${ decoders . length } > ${ data . length }` ) ; } return decoders . map ( ( decoder , index ) => decoder . forceDecode ( data [ index ] ) ) as any as D ; } } ) ; function checkDictType ( data ) { if ( typeof data !== 'object' || data === null || Array . isArray ( data ) ) { throw new DecoderError ( `This is not an object: ${ show ( data ) }` ) ; } else if ( Object . keys ( data ) . some ( $ => typeof $ !== 'string' ) ) { throw new DecoderError ( `Not all keys in this object are strings: ${ show ( data ) }` ) ; } } export const record = < D > ( decoder ) : Decoder < Record < string , D > > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; checkDictType ( data ) ; return Object . fromEntries ( Object . entries ( data ) . map ( ( [ key , value ] ) => [ key , decoder . forceDecode ( value ) ] ) ) ; } } ) ; export const keyValuePairs = < D > ( decoder ) : Decoder < Array < [ string , D ] > > => createDecoder ( { forceDecode : ( data ) => Object . entries ( record ( decoder ) . forceDecode ( data ) ) } ) ; export type DecoderRecord = Record < PropertyKey , Decoder < any > > ; type OmitEmptyPartial < T extends DecoderRecord > = T extends infer U & Partial < { [ x ] : any ; } > ? U : never ; type ObjectType < D extends DecoderRecord > = D extends { [ K in keyof infer U ] : Decoder < ( infer U ) [ K ] > ; } ? U : never ; const required = < D extends DecoderRecord > ( struct ) : Decoder < ObjectType < D > > => createDecoder ( { forceDecode : ( data ) => { checkDictType ( data ) ; const parsed = { } ; for ( const key in struct ) { if ( data [ key ] === undefined ) throw new DecoderError ( `Object missing required property '${ key }'` ) ; parsed [ key ] = struct [ key ] . forceDecode ( data [ key ] ) ; } return parsed as ObjectType < D > ; } } ) ; const partial = < D extends DecoderRecord > ( struct ) : Decoder < Partial < ObjectType < D > > > => createDecoder ( { forceDecode : ( data ) => { checkDictType ( data ) ; const parsed = { } ; for ( const key in struct ) { if ( data [ key ] !== undefined ) { parsed [ key ] = struct [ key ] . forceDecode ( data [ key ] ) ; } } return parsed ; } } ) ; export const object = < D extends DecoderRecord , E extends DecoderRecord > ( struct ) : Decoder < OmitEmptyPartial < ObjectType < D > & Partial < ObjectType < E > > > > => createDecoder ( { forceDecode : ( data ) => { checkDefined ( data ) ; const result = { } ; if ( struct . required !== undefined ) { Object . assign ( result , required ( struct . required ) . forceDecode ( data ) ) ; } if ( struct . optional !== undefined ) { Object . assign ( result , partial ( struct . optional ) . forceDecode ( data ) ) ; } return result as OmitEmptyPartial < ObjectType < D > & Partial < ObjectType < E > > > ; } } ) ; export const recursive = < D > ( decoder ) : Decoder < D > => createDecoder ( { forceDecode : ( data ) => { return decoder ( ) . forceDecode ( data ) ; } } ) ;