@apoyo/decoders
Version:
Validation utilities
1,534 lines (1,523 loc) • 53 kB
TypeScript
import * as _apoyo_std from '@apoyo/std';
import { Dict, Tree, Result, Option, Refinement, Predicate, InverseRefinement, Enum } from '@apoyo/std';
declare const _uuid: unique symbol;
declare const _email: unique symbol;
declare const _date: unique symbol;
declare const _datetime: unique symbol;
declare const _int: unique symbol;
interface UUIDBrand {
readonly [_uuid]: unknown;
}
interface EmailBrand {
readonly [_email]: unknown;
}
interface DateBrand {
readonly [_date]: unknown;
}
interface DatetimeBrand {
readonly [_datetime]: unknown;
}
interface IntBrand {
readonly [_int]: unknown;
}
/**
* An UUID
*/
declare type UUID = string & UUIDBrand;
/**
* An email
*/
declare type Email = string & EmailBrand;
/**
* A number without a decimal point
*/
declare type Int = number & IntBrand;
declare namespace ISO {
/**
* A date string with the following format:
*
* YYYY-MM-DD
*/
type Date = string & DateBrand;
/**
* An ISO datetime string with the following format:
*
* YYYY-MM-DD HH:mm:ssZ
*/
type Datetime = string & DatetimeBrand;
}
declare const enum ErrorCode {
REQUIRED = "required",
STRING = "string",
STRING_LENGTH = "string.length",
STRING_MIN = "string.min",
STRING_MAX = "string.max",
STRING_PATTERN = "string.pattern",
STRING_EMAIL = "string.email",
STRING_UUID = "string.uuid",
STRING_EQUALS = "string.equals",
STRING_ONE_OF = "string.oneOf",
STRING_DATE = "string.date",
STRING_DATETIME = "string.datetime",
DATE = "date",
DATE_STRICT = "date.strict",
DATE_MIN = "date.min",
DATE_MAX = "date.max",
NUMBER = "number",
NUMBER_STRICT = "number.strict",
NUMBER_FROM_STRING = "number.fromString",
NUMBER_MIN = "number.min",
NUMBER_MAX = "number.max",
INT = "int",
INT_STRICT = "int.strict",
INT_FROM_STRING = "int.fromString",
INT_MIN = "int.min",
INT_MAX = "int.max",
BOOL = "bool",
BOOL_STRICT = "bool.strict",
BOOL_FROM_STRING = "bool.fromString",
BOOL_FROM_NUMBER = "bool.fromNumber",
BOOL_EQUALS = "bool.equals",
ARRAY = "array",
ARRAY_NON_EMPTY = "array.nonEmpty",
ARRAY_LENGTH = "array.length",
ARRAY_MIN = "array.min",
ARRAY_MAX = "array.max",
DICT = "dict",
ENUM = "enum.native",
ENUM_LITERAL = "enum.literal",
ENUM_IS_IN = "enum.isIn"
}
declare const enum DecodeErrorTag {
VALUE = "DE.Value",
ARRAY = "DE.ArrayLike",
INDEX = "DE.Index",
OBJECT = "DE.ObjectLike",
KEY = "DE.Key",
UNION = "DE.UnionLike",
MEMBER = "DE.Member"
}
declare namespace DecodeError {
interface Value {
tag: DecodeErrorTag.VALUE;
value: unknown;
message: string;
meta: Dict;
}
interface Key {
tag: DecodeErrorTag.KEY;
key: string;
error: DecodeError;
}
interface Index {
tag: DecodeErrorTag.INDEX;
index: number;
error: DecodeError;
}
interface Member {
tag: DecodeErrorTag.MEMBER;
index: number;
error: DecodeError;
}
interface ArrayLike {
tag: DecodeErrorTag.ARRAY;
kind: string;
errors: DecodeError.Index[];
}
interface ObjectLike {
tag: DecodeErrorTag.OBJECT;
kind: string;
name?: string;
errors: DecodeError.Key[];
}
interface UnionLike {
tag: DecodeErrorTag.UNION;
kind: string;
name?: string;
errors: DecodeError.Member[];
}
type Path = {
tag: DecodeErrorTag.ARRAY;
kind: string;
} | {
tag: DecodeErrorTag.INDEX;
index: number;
} | {
tag: DecodeErrorTag.OBJECT;
kind: string;
name?: string;
} | {
tag: DecodeErrorTag.KEY;
key: string;
} | {
tag: DecodeErrorTag.UNION;
kind: string;
name?: string;
} | {
tag: DecodeErrorTag.MEMBER;
index: number;
};
interface Flat {
value: unknown;
message: string;
meta: Dict;
path: Path[];
}
interface Formatted {
value: unknown;
message: string;
description: string;
meta: Dict;
path: string;
}
}
declare type DecodeError = DecodeError.Value | DecodeError.ObjectLike | DecodeError.ArrayLike | DecodeError.UnionLike;
declare const DecodeError: {
value: (value: unknown, message: string, meta?: Dict) => DecodeError.Value;
key: (key: string, error: DecodeError) => DecodeError.Key;
index: (index: number, error: DecodeError) => DecodeError.Index;
member: (index: number, error: DecodeError) => DecodeError.Member;
array: (errors: DecodeError.Index[]) => DecodeError.ArrayLike;
object: (errors: DecodeError.Key[], name?: string) => DecodeError.ObjectLike;
union: (errors: DecodeError.Member[], name?: string) => DecodeError.UnionLike;
fold: <T>(cases: {
value(err: DecodeError.Value): T;
array(err: DecodeError.ArrayLike): T;
object(err: DecodeError.ObjectLike): T;
union(err: DecodeError.UnionLike): T;
}) => (err: DecodeError) => T;
toTree: (e: DecodeError) => Tree<string>;
/**
* @description
* Format your `DecodeError` into a easily displayable string.
*/
draw: (e: DecodeError) => string;
/**
* @description
* Flatten your `DecodeError` into an `DecodeError.Flat`.
* This allows you to more easily format custom errors.
*
* It is however recommended to simply use `DecodeError.format` or `DecodeError.formatBy` instead.
*/
flatten: (e: DecodeError) => DecodeError.Flat[];
/**
* @description
* Get full error description / stack from a `DecodeError.Flat`.
* Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat`
*/
getDescription: (err: DecodeError.Flat, separator?: string) => string;
/**
* @description
* Get full property path from a `DecodeError.Flat`.
* Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat`
*/
getPath: (err: DecodeError.Flat) => string;
/**
* @description
* Get formatted error from a `DecodeError.Flat`.
* Use `DecodeError.flatten` to transform your `DecodeError` into an `DecodeError.Flat`
*/
getFormatted: (e: DecodeError.Flat) => DecodeError.Formatted;
/**
* @description
* Create a custom error formatter, to fully customize your error output
*/
formatBy: <T_1>(fn: (flat: DecodeError.Flat) => T_1) => (e: DecodeError) => T_1[];
/**
* @description
* Default `DecodeError.formatBy` implementation, that returns `DecodeError.Formatted` errors
*/
format: (e: DecodeError) => DecodeError.Formatted[];
/**
* @deprecated Use `DecodeError.getDescription` instead
*
* @see `DecodeError.getDescription`
*/
formatError: (e: DecodeError.Flat) => string;
};
declare type DecoderResult<A> = Result<A, DecodeError>;
declare function filter<A, B extends A>(fn: Refinement<A, B>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, B>;
declare function filter<A>(fn: Predicate<A>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, A>;
declare function reject<A, B extends A>(fn: Refinement<A, B>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, InverseRefinement<A, B>>;
declare function reject<A>(fn: Predicate<A>, message: string, meta?: Dict<unknown>): <I>(value: Decoder<I, A>) => Decoder<I, A>;
declare function validate<O>(decoder: Decoder<unknown, O>): (input: unknown) => DecoderResult<O>;
declare function validate<I, O>(decoder: Decoder<I, O>): (input: I) => DecoderResult<O>;
declare function defaultValue(value: never[]): <I, O extends any[]>(decoder: Decoder<I, O | undefined>) => Decoder<I, O>;
declare function defaultValue<T>(value: T): <I, O>(decoder: Decoder<I, O | undefined>) => Decoder<I, O | T>;
declare function union<I, O1, O2>(a: Decoder<I, O1>, b: Decoder<I, O2>): Decoder<I, O1 | O2>;
declare function union<I, O1, O2, O3>(a: Decoder<I, O1>, b: Decoder<I, O2>, c: Decoder<I, O3>): Decoder<I, O1 | O2 | O3>;
declare function union<I, O1, O2, O3, O4>(a: Decoder<I, O1>, b: Decoder<I, O2>, c: Decoder<I, O3>, d: Decoder<I, O4>): Decoder<I, O1 | O2 | O3 | O4>;
declare type Decoder<I, O> = {
decode(input: I): DecoderResult<O>;
};
declare namespace Decoder {
type TypeOf<A> = A extends Decoder<unknown, infer B> ? Option.Struct<B> : never;
type InputOf<A> = A extends Decoder<infer B, unknown> ? Option.Struct<B> : never;
}
/**
* @namespace Decoder
*
* @description
*
* A `Decoder` is a function, that from an input I to create an output O, or produce an DecodeError.
* As such, a common use-case for `Decoder`s are type and value validations.
*
* This namespace contains the core utilities to:
* - Create `Decoder`s
* - Use or combine them
* - Extract / infer the resulting type informations
*
* @example
* ```ts
* export const TodoDto = ObjectDecoder.struct({
* id: IntegerDecoder.int,
* title: TextDecoder.varchar(1, 100),
* description: pipe(
* TextDecoder.varchar(0, 2000),
* TextDecoder.nullable
* ),
* done: BooleanDecoder.boolean
* })
*
* export interface TodoDto extends Decoder.TypeOf<typeof TodoDto> {}
* ```
*/
declare const Decoder: {
/**
* @description
* Create a new decoder
*/
create: <I, O>(fn: (input: I) => DecoderResult<O>) => Decoder<I, O>;
/**
* @description
* Creates a new decoder from a type guard
*
* @example
* ```ts
* const stringDecoder = Decoder.fromGuard(
* (input: unknown): input is string => typeof input === 'string',
* 'value is not a string'
* )
*
* expect(pipe('Hello', Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(42, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
fromGuard: <I_1, O_1 extends I_1>(fn: Refinement<I_1, O_1>, message: string, meta?: Dict<unknown>) => Decoder<I_1, O_1>;
/**
* @description
* Map over the resulting value of an `Decoder`
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.map(str => str.trim())
* )
*
* expect(pipe(' Hello ', Decoder.validate(decoder), Result.get)).toBe('Hello')
* ```
*/
map: <A, B>(fn: (input: A) => B) => <I_2>(decoder: Decoder<I_2, A>) => Decoder<I_2, B>;
/**
* @description
* Map over the resulting error of an `Decoder`
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.mapError(err => DecodeError.object([
* DecodeError.key('firstName', err)
* ]))
* )
* ```
*/
mapError: <I_3>(fn: (err: DecodeError, input: I_3) => DecodeError) => <A_1>(decoder: Decoder<I_3, A_1>) => Decoder<I_3, A_1>;
/**
* @description
* Catch the validation error and create a new error with the given message.
*
* @example
* ```ts
* const decoder = pipe(
* Decoder.union(
* NumberDecoder.number,
* NumberDecoder.fromString
* ),
* Decoder.withMessage('The given value is not a number', {
* code: 'invalid_number'
* })
* )
*
* const expectedError = DecodeError.value(' Hello ', 'This value is not a number', {
* code: 'invalid_number'
* })
* expect(pipe(' Hello ', Decoder.validate(decoder))).toEqual(Result.ko(expectedError))
* ```
*/
withMessage: (msg: string, meta?: Dict<unknown>) => <I_4, A_2>(decoder: Decoder<I_4, A_2>) => Decoder<I_4, A_2>;
/**
* @description
* Add a custom validation function to an `Decoder`.
*
* Compared to `Decoder.guard`, this function gives more control and allows the user to modify the resulting value.
*
* @see `Decoder.guard`
* @see `Decoder.filter`
* @see `Decoder.reject`
*
* @example
* ```ts
* const validateAge = (date: Date): Result<string, DecodeError> => {
* const now = new Date()
*
* if (date.getFullYear() < now.getFullYear() - 100) {
* return Result.ko(DecodeError.value(dob, 'Date of birth is more than 100 years ago'))
* }
* if (date.getFullYear() > now.getFullYear() - 18) {
* return Result.ko(DecodeError.value(dob, 'Date of birth is less than 18 years ago'))
* }
* return Result.ok(dob)
* }
*
* const birthdayDecoder = pipe(
* DateDecoder.date,
* Decoder.parse(validateAge)
* )
*
* expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true)
* expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true)
* ```
*/
parse: <B_1, C>(fn: (input: B_1) => DecoderResult<C>) => <A_3>(decoder: Decoder<A_3, B_1>) => Decoder<A_3, C>;
/**
* @description
* Chain another decoder to execute with the current input.
* This allows you to dynamically compute the decoder to use depending on a value.
*/
chain: <B_2, C_1>(fn: (input: B_2) => Decoder<B_2, C_1>) => <A_4>(decoder: Decoder<A_4, B_2>) => Decoder<A_4, C_1>;
/**
* @description
* Add a custom validation function, returning either:
* - A `DecodeError` if the input is incorrect
* - `undefined` if there is no error to report.
*
* This function gives more control about the returned error than `Decoder.filter` or `Decoder.reject`, but does not allow the value to be modified.
*
* @see `Decoder.parse`
* @see `Decoder.filter`
* @see `Decoder.reject`
*
* @example
* ```ts
* const validateAge = (date: Date): Option<DecodeError> => {
* const now = new Date()
*
* if (date.getFullYear() < now.getFullYear() - 100) {
* return DecodeError.value(dob, 'Date of birth is more than 100 years ago')
* }
* if (date.getFullYear() > now.getFullYear() - 18) {
* return DecodeError.value(dob, 'Date of birth is less than 18 years ago')
* }
* return undefined
* }
*
* const birthdayDecoder = pipe(
* DateDecoder.date,
* Decoder.guard(validateAge)
* )
*
* expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true)
* expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true)
* ```
*/
guard: <O_2>(fn: (input: O_2) => Option<DecodeError>) => <A_5>(decoder: Decoder<A_5, O_2>) => Decoder<A_5, O_2>;
/**
* @description
* Add a `Predicate` filter function to a `Decoder`.
* If the value matches the `Predicate`, the value is kept.
* Else, an DecodeError with the given message is returned.
*
* @see `Decoder.parse`
* @see `Decoder.guard`
* @see `Decoder.reject`
*
* @see `DateDecoder.min`
* @see `DateDecoder.max`
*
* @example
* ```ts
* const maxAge = (age: number) => (dob: Date) => {
* const now = new Date()
* return date.getFullYear() > now.getFullYear() - age
* }
* const minAge = (age: number) => (dob: Date) => {
* const now = new Date()
* return date.getFullYear() < now.getFullYear() - age
* }
*
* const birthdayDecoder = pipe(
* DateDecoder.date,
* Decoder.filter(maxAge(100), `Date of birth is more than 100 years ago`),
* Decoder.filter(minAge(18), `Date of birth is less than 18 years ago`)
* )
*
* expect(pipe('1930-01-01', Decoder.validate(birthdayDecoder), Result.isOk)).toBe(true)
* expect(pipe('1920-01-01', Decoder.validate(birthdayDecoder), Result.isKo)).toBe(true)
* ```
*/
filter: typeof filter;
/**
* @description
* Add a `Predicate` filter function to a `Decoder`.
* If the value matches the `Predicate`, an DecodeError with the given message is returned.
* Else, the value is kept.
*
* @see `Decoder.parse`
* @see `Decoder.guard`
* @see `Decoder.filter`
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.reject(str => str.length === 0, `string should not be empty`)
* )
*
* expect(pipe('Hello', Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
reject: typeof reject;
/**
* @description
* Makes the value nullable. If the input is undefined, the decoder will return null instead.
*
* **Note**: If you want to transform an empty string to `null`, use `TextDecoder.nullable` instead.
*
* @see `Decoder.optional`
* @see `TextDecoder.nullable`
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.nullable
* )
*
* expect(pipe('Hello', Decoder.validate(decoder), Result.get)).toBe('Hello')
* expect(pipe('', Decoder.validate(decoder), Result.get)).toBe('')
* expect(pipe(null, Decoder.validate(decoder), Result.get)).toBe(null)
* expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(null)
* ```
*/
nullable: <I_5, O_3>(decoder: Decoder<I_5, O_3>) => Decoder<I_5, O_3 | null>;
/**
* @description
* Makes the value optional (allows `undefined`).
*
* **Note**: If you want to transform an empty string to `undefined`, use `TextDecoder.optional` instead.
*
* @see `Decoder.nullable`
* @see `TextDecoder.optional`
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.optional
* )
*
* expect(pipe('Hello', Decoder.validate(decoder), Result.get)).toBe('Hello')
* expect(pipe('', Decoder.validate(decoder), Result.get)).toBe('')
* expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(undefined)
* ```
*/
optional: <I_6, O_4>(decoder: Decoder<I_6, O_4>) => Decoder<I_6, O_4 | undefined>;
/**
* @description
* Explicitely return an "input is required" error when input is "null" or "undefined"
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* Decoder.required
* )
* ```
*/
required: <I_7, O_5>(decoder: Decoder<I_7, O_5>) => Decoder<I_7, O_5>;
/**
* @description
* This function allows the creation of recursive type decoders.
*
* @example
* ```ts
* interface Tree<T> {
* value: T
* forest: Tree<T>[]
* }
*
* // Recursive types require manual typing
* const TreeDecoder = <O>(decoder: Decoder<unknown, O>): Decoder<unknown, Tree<O>> =>
* Decoder.lazy(() =>
* ObjectDecoder.struct({
* value: decoder,
* forest: ArrayDecoder.array(TreeDecoder(decoder))
* })
* )
*
* const input: unknown = {
* value: 'Hello',
* forest: [
* {
* value: 'World',
* forest: []
* }
* ]
* }
*
* expect(pipe(input, TreeDecoder(TextDecoder.string), Result.isOk)).toBe(true)
* ```
*/
lazy: <I_8, O_6>(fn: () => Decoder<I_8, O_6>) => Decoder<I_8, O_6>;
/**
* @description
* This operator makes the decoder optional and returns the given value when the input is undefined.
*
* @example
* ```ts
* const decoder = pipe(
* BooleanDecoder.boolean,
* Decoder.default(false)
* )
*
* expect(pipe(true, Decoder.validate(decoder), Result.get)).toEqual(true)
* expect(pipe(false, Decoder.validate(decoder), Result.get)).toEqual(false)
* expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toEqual(false)
* ```
*/
default: typeof defaultValue;
/**
* @description
* Creates a union `Decoder` that tries, in the given order, if the input is valid.
*
* @example
* ```ts
* const decoder = Decoder.union(
* NumberDecoder.number,
* NumberDecoder.fromString
* )
*
* expect(pipe(42, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe("42", Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe("Hello", Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
union: typeof union;
/**
* @description
* Utility to bind a given generated type to a `Decoder`
*/
ref: <A_6>(decoder: Decoder<unknown, A_6>) => Decoder<unknown, A_6>;
/**
* @description
* Validate an input by the given `Decoder`
*
* @example
* ```
* const result = pipe(
* input,
* Decoder.validate(TextDecoder.string)
* )
*
* if (Result.isKo(result)) {
* console.log(result.ko)
* return
* }
*
* const value = result.ok
* console.log(value)
* ```
*/
validate: typeof validate;
/**
* @description
* `Decoder` for an unknown value
*/
unknown: Decoder<unknown, unknown>;
/**
* @description
* `Decoder` for any value
*/
any: Decoder<unknown, any>;
};
declare function oneOf<T extends string>(arr: T[]): Decoder<unknown, T>;
declare function oneOf<T extends string>(arr: Set<T>): Decoder<unknown, T>;
declare type TextDecoder<I> = Decoder<I, string>;
/**
* @namespace TextDecoder
*
* @description
* This namespace contains string decoders and additional utilities for string validations.
*/
declare const TextDecoder: {
/**
* @description
* Check if the input is a string
*/
string: TextDecoder<unknown>;
/**
* @description
* Check the length of the string
*/
length: (len: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>;
/**
* @description
* Check the minimum length of the string
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.min(1)
* )
*
* expect(pipe('1', Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
min: (minLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>;
/**
* @description
* Check the maximum length of the string
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.max(5)
* )
*
* expect(pipe('12345', Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe('123456', Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
max: (maxLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>;
/**
* @description
* Check both the minimum and maximum length of the string
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.between(1, 100)
* )
*
* expect(pipe('', Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
between: (minLength: number, maxLength: number) => <I>(value: Decoder<I, string>) => Decoder<I, string>;
/**
* @description
* Trim the string
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.trim,
* TextDecoder.between(1, 100)
* )
*
* expect(pipe(' ', Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
trim: <I_1>(decoder: Decoder<I_1, string>) => Decoder<I_1, string>;
/**
* @description
* Check if the string matches a given pattern / regexp
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.pattern(/^Hello [a-zA-Z]+$/)
* )
*
* expect(pipe('Hello world', Decoder.validate(decoder), Result.isOk)).toBe(true)
* ```
*/
pattern: <T extends string = string>(regexp: RegExp, message?: string, meta?: Dict) => <I_2>(value: Decoder<I_2, string>) => Decoder<I_2, T>;
/**
* @description
* Check if the string is a date in the ISO format (YYYY-MM-DD)
*
* @example
* ```ts
* expect(pipe("2020-06-13", Decoder.validate(TextDecoder.date), Result.isOk)).toBe(true)
* expect(pipe("Hello", Decoder.validate(TextDecoder.date), Result.isKo)).toBe(true)
* ```
*/
date: Decoder<unknown, ISO.Date>;
/**
* @description
* Check if the string is a datetime in the ISO format (YYYY-MM-DD HH:mm:ss)
*
* @example
* ```ts
* expect(pipe("2020-06-13", Decoder.validate(TextDecoder.datetime), Result.isOk)).toBe(true)
* expect(pipe("2020-06-13 13:53:23Z", Decoder.validate(TextDecoder.datetime), Result.isOk)).toBe(true)
* ```
*/
datetime: Decoder<unknown, ISO.Datetime>;
/**
* @description
* Check if the input is an email
*
* @example
* ```ts
* expect(pipe("test@example.com", Decoder.validate(TextDecoder.email), Result.isOk)).toBe(true)
* expect(pipe("test", Decoder.validate(TextDecoder.email), Result.isKo)).toBe(true)
* ```
*/
email: Decoder<unknown, Email>;
/**
* @description
* Check if the input is an UUID
*/
uuid: Decoder<unknown, UUID>;
/**
* @description
* Check if the input is a string between the given length.
*
* @example
* ```ts
* // The following:
* const decoder = TextDecoder.varchar(1, 100)
* // is the same as:
* const decoder = pipe(TextDecoder.string, TextDecoder.between(1, 100))
* ```
*/
varchar: (minLength: number, maxLength: number) => Decoder<unknown, string>;
/**
* @description
* This makes the string nullable. If the string is empty, `null` is returned.
*
* @see `TextDecoder.optional`
* @see `Decoder.nullable`
*
* @example
* ```ts
* const decoder = pipe(TextDecoder.string, TextDecoder.nullable)
*
* expect(pipe("Hello", Decoder.validate(decoder), Result.get)).toBe("Hello")
* expect(pipe(null, Decoder.validate(decoder), Result.get)).toBe(null)
* expect(pipe("", Decoder.validate(decoder), Result.get)).toBe(null)
* ```
*/
nullable: <I_3>(decoder: Decoder<I_3, string>) => Decoder<I_3, string | null>;
/**
* @description
* This makes the string optional (allows `undefined`). If the string is empty, `undefined` is returned.
*
* @see `TextDecoder.nullable`
* @see `Decoder.optional`
*
* @example
* ```ts
* const decoder = pipe(TextDecoder.string, TextDecoder.optional)
*
* expect(pipe("Hello", Decoder.validate(decoder), Result.get)).toBe("Hello")
* expect(pipe(undefined, Decoder.validate(decoder), Result.get)).toBe(undefined)
* expect(pipe("", Decoder.validate(decoder), Result.get)).toBe(undefined)
* ```
*/
optional: <I_4>(decoder: Decoder<I_4, string>) => Decoder<I_4, string | undefined>;
/**
* @deprecated Use `EnumDecoder.isIn` instead.
*
* @description
* Check if the string is included in the given values
*
* @example
* ```ts
* const decoder = TextDecoder.oneOf(['todo', 'in-progress', 'done', 'archived'] as const)
*
* expect(pipe("todo", Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe("unknown", Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
oneOf: typeof oneOf;
/**
* @deprecated Use `EnumDecoder.literal` instead.
*
* @description
* Check if the string is included in the given values
*
* @example
* ```ts
* const decoder = TextDecoder.equals('ongoing')
*
* expect(pipe("todo", Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe("unknown", Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
equals: <T_1 extends string>(value: T_1) => Decoder<unknown, T_1>;
/**
* @description
* Escapes the HTML in the string.
*
* @example
* ```ts
* const decoder = pipe(
* TextDecoder.string,
* TextDecoder.trim,
* TextDecoder.htmlEscape,
* TextDecoder.between(1, 2000)
* )
*
* const escaped = pipe(
* "<script>window.alert("Hello")</script>",
* Decoder.validate(decoder),
* Result.get
* )
*
* expect(escaped).toBe('<script>window.alert("Hello")</script>')
* ```
*/
htmlEscape: <I_1>(decoder: Decoder<I_1, string>) => Decoder<I_1, string>;
};
declare type NumberDecoder<I> = Decoder<I, number>;
/**
* @namespace NumberDecoder
*
* @description
* This namespace contains number decoders and additional utilities for number validations.
*/
declare const NumberDecoder: {
/**
* @description
* Check if the input is a number.
* If the number is NaN, the number will fail validation.
*
* This function is strict and does not autocast the input into a number.
*/
strict: NumberDecoder<unknown>;
/**
* @description
* Check if the input is a number.
* If the number is NaN, the number will fail validation.
*
* This function will autocast the input into a number if possible.
*/
number: NumberDecoder<unknown>;
/**
* @description
* Check if the number is equal or greater to the given minimum
*
* @example
* ```ts
* const decoder = pipe(
* NumberDecoder.number,
* NumberDecoder.min(1)
* )
*
* expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
min: (minimum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>;
/**
* @description
* Check if the number is equal or greater to the given minimum
*
* @example
* ```ts
* const decoder = pipe(
* NumberDecoder.number,
* NumberDecoder.max(100)
* )
*
* expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
max: (maximum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>;
/**
* @description
* Check if the number is between to the given minimum and maximum
*
* @example
* ```ts
* const decoder = pipe(
* NumberDecoder.number,
* NumberDecoder.between(1, 100)
* )
*
* expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true)
* expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
between: (minimum: number, maximum: number) => <I>(value: Decoder<I, number>) => Decoder<I, number>;
/**
* @description
* Check if the input is a number between the given minimum and maximum.
*
* @example
* ```ts
* // The following:
* const decoder = NumberDecoder.range(1, 100)
* // is the same as:
* const decoder = pipe(NumberDecoder.number, NumberDecoder.between(1, 100))
* ```
*/
range: (minimum: number, maximum: number) => Decoder<unknown, number>;
/**
* @description
* Decodes an input from a string into a number.
*
* @example
* ```
* expect(pipe('10.13', Decoder.validate(NumberDecoder.fromString), Result.get)).toBe(10.13)
* expect(pipe('Hello', Decoder.validate(NumberDecoder.fromString), Result.isKo)).toBe(true)
* ```
*/
fromString: Decoder<unknown, number>;
};
declare type IntegerDecoder<I> = Decoder<I, Int>;
/**
* @namespace IntegerDecoder
*
* @description
* This namespace contains integer decoders and additional utilities for integer validations.
*/
declare const IntegerDecoder: {
/**
* @description
* Check if the input is an integer.
* If the integer is NaN or contains decimals, the number will fail validation.
*
* This function is strict and does not autocast the input into a number.
*/
strict: IntegerDecoder<unknown>;
/**
* @description
* Check if the input is an integer.
* If the integer is NaN or contains decimals, the number will fail validation.
*
* This function will autocast the input into a number if possible.
*/
int: IntegerDecoder<unknown>;
/**
* @description
* Check if the number is equal or greater to the given minimum
*
* @example
* ```ts
* const decoder = pipe(
* IntegerDecoder.number,
* IntegerDecoder.min(1)
* )
*
* expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
min: (minimum: number) => <I>(decoder: IntegerDecoder<I>) => IntegerDecoder<I>;
/**
* @description
* Check if the number is equal or greater to the given minimum
*
* @example
* ```ts
* const decoder = pipe(
* IntegerDecoder.number,
* IntegerDecoder.max(100)
* )
*
* expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
max: (maximum: number) => <I_1>(decoder: IntegerDecoder<I_1>) => IntegerDecoder<I_1>;
/**
* @description
* Check if the number is between to the given minimum and maximum
*
* @example
* ```ts
* const decoder = pipe(
* IntegerDecoder.number,
* IntegerDecoder.between(1, 100)
* )
*
* expect(pipe(1, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(100, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(0, Decoder.validate(decoder), Result.isKo)).toBe(true)
* expect(pipe(101, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
between: (minimum: number, maximum: number) => <I_2>(decoder: IntegerDecoder<I_2>) => IntegerDecoder<I_2>;
/**
* @description
* Check if the input is a number between the given minimum and maximum.
*
* @example
* ```ts
* // The following:
* const decoder = IntegerDecoder.range(1, 100)
* // is the same as:
* const decoder = pipe(IntegerDecoder.number, IntegerDecoder.between(1, 100))
* ```
*/
range: (minimum: number, maximum: number) => IntegerDecoder<unknown>;
/**
* @description
* Check if the input is a positive integer.
* If the integer is NaN or contains decimals, the number will fail validation.
*/
positive: IntegerDecoder<unknown>;
/**
* @description
* Decodes an input from a string into an integer.
*
* @example
* ```
* expect(pipe('10', Decoder.validate(IntegerDecoder.fromString), Result.get)).toBe(10)
* expect(pipe('Hello', Decoder.validate(IntegerDecoder.fromString), Result.isKo)).toBe(true)
* ```
*/
fromString: Decoder<unknown, Int>;
};
declare type BooleanDecoder<I> = Decoder<I, boolean>;
/**
* @namespace BooleanDecoder
*
* @description
* This namespace contains boolean decoders and additional utilities for boolean validations.
*/
declare const BooleanDecoder: {
/**
* @description
* Check if the input is a boolean.
*
* This function is strict and does not autocast the input into a boolean.
*/
strict: BooleanDecoder<unknown>;
/**
* @description
* Check if the input is a boolean.
*
* This function will autocast the input into a boolean if possible.
*/
boolean: BooleanDecoder<unknown>;
/**
* @description
* Check if the boolean is true or false
*
* @example
* ```
* const decoder = BooleanDecoder.equals(true)
*
* expect(pipe(true, Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe(false, Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
equals: <T extends boolean>(bool: T) => Decoder<unknown, T>;
/**
* @description
* Decodes an input from a string into a boolean:
* - 'true', 'yes', 'y', '1' becomes true
* - 'false', 'no', 'n', '0' becomes false
*
* Any other value will fail validation.
*
* @example
* ```
* expect(pipe('yes', Decoder.validate(BooleanDecoder.fromString), Result.get)).toBe(true)
* ```
*/
fromString: Decoder<unknown, boolean>;
/**
* @description
* Decodes an input from a number into a boolean:
* - 1 becomes true
* - 0 becomes false
*
* Any other value will fail validation.
*
* @example
* ```
* expect(pipe(1, Decoder.validate(BooleanDecoder.fromNumber), Result.get)).toBe(true)
* ```
*/
fromNumber: Decoder<unknown, boolean>;
};
declare type Struct<A extends Dict<unknown>> = {
[P in keyof A]-?: Decoder<unknown, A[P]>;
};
declare function omit<I, O extends Dict, B extends keyof O>(props: B[]): (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, Omit<O, B>>;
declare function pick<I, O extends Dict, B extends keyof O>(props: B[]): (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, Pick<O, B>>;
declare function partial<I, O extends Dict>(decoder: ObjectDecoder<I, O>): ObjectDecoder<I, Partial<O>>;
declare function merge<I, O1 extends Dict>(a: ObjectDecoder<I, O1>): ObjectDecoder<I, O1>;
declare function merge<I, O1 extends Dict, O2 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>): ObjectDecoder<I, O2 & Omit<O1, keyof O2>>;
declare function merge<I, O1 extends Dict, O2 extends Dict, O3 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>, c: ObjectDecoder<I, O3>): ObjectDecoder<I, O3 & Omit<O2, keyof O3> & Omit<O1, keyof O3 | keyof O2>>;
declare function merge<I, O1 extends Dict, O2 extends Dict, O3 extends Dict, O4 extends Dict>(a: ObjectDecoder<I, O1>, b: ObjectDecoder<I, O2>, c: ObjectDecoder<I, O3>, d: ObjectDecoder<I, O4>): ObjectDecoder<I, O4 & Omit<O3, keyof O4> & Omit<O2, keyof O4 | keyof O3> & Omit<O1, keyof O4 | keyof O3 | keyof O2>>;
declare type SumTypes<K extends string, T extends Dict> = {
[P in keyof T]: T[P] extends ObjectDecoder<any, infer A> ? {
[KP in K]: P;
} & A : never;
}[keyof T];
declare function sum<K extends string, I, T extends Dict<ObjectDecoder<I, any>>>(prop: K, cases: T): Decoder<I, SumTypes<K, T>>;
declare type ObjectDecoder<I, O extends Dict> = Decoder<I, O> & {
props: Dict;
};
/**
* @namespace ObjectDecoder
*
* @description
* This namespace contains object decoders and additional utilities for object validations.
*/
declare const ObjectDecoder: {
/**
* @description
* Check if the input is an object / record.
* This function does not check the type of the properties.
*/
unknownDict: Decoder<unknown, Dict<unknown>>;
/**
* @description
* Check if the input is an record, where all properties are of the given type.
*/
dict: <A>(decoder: Decoder<unknown, A>) => Decoder<unknown, Dict<A>>;
/**
* @description
* Check if the input is an object, where all object properties match the given decoders.
* All extraenous properties will be skipped and ignored.
*
* @example
* ```ts
* const TodoDto = ObjectDecoder.struct({
* id: IntegerDecoder.positive,
* title: TextDecoder.varchar(1, 100),
* done: BooleanDecoder.boolean
* })
*
* const input: unknown = {
* id: 0,
* title: 'Wake up',
* description: 'Some description', // This property is not recognized by the decoder and will be ignored.
* done: false
* }
*
* expect(pipe(input, Decoder.validate(TodoDto), Result.isOk)).toBe(true)
* expect(pipe(input, Decoder.validate(TodoDto), Result.get)).toEqual({
* id: 0,
* title: 'Wake up',
* done: false
* })
* ```
*/
struct: <A_1 extends Dict<any>>(props: Struct<A_1>, name?: string) => ObjectDecoder<unknown, A_1>;
/**
* @description
* Omit given properties from an `ObjectDecoder`.
* The resulting `ObjectDecoder` will not contain the omitted properties.
*
* @example
* ```ts
* const TodoPostDto = pipe(TodoDto, ObjectDecoder.omit(['id']))
*
* const input: unknown = {
* id: 0, // This property has been omitted and will be ignored.
* title: 'Wake up',
* done: false
* }
*
* expect(pipe(input, Decoder.validate(TodoPostDto), Result.isOk)).toBe(true)
* expect(pipe(input, Decoder.validate(TodoPostDto), Result.get)).toEqual({
* title: 'Wake up',
* done: false
* })
* ```
*/
omit: typeof omit;
/**
* @description
* Pick given properties from an `ObjectDecoder`.
* The resulting `ObjectDecoder` will only contain the picked properties.
*
* @example
* ```ts
* const TodoPostDto = pipe(TodoDto, ObjectDecoder.pick(['title', 'done']))
*
* const input: unknown = {
* id: 0, // This property has not been picked and will be ignored.
* title: 'Wake up',
* done: false
* }
*
* expect(pipe(input, Decoder.validate(TodoPostDto), Result.isOk)).toBe(true)
* expect(pipe(input, Decoder.validate(TodoPostDto), Result.get)).toEqual({
* title: 'Wake up',
* done: false
* })
* ```
*/
pick: typeof pick;
/**
* @description
* Make all properties of an `ObjectDecoder` optional.
*/
partial: typeof partial;
/**
* @description
* Add a custom validation function to the `ObjectDecoder`.
*
* @example
* ```ts
* const SignupDto = pipe(
* ObjectDecoder.struct({
* email: TextDecoder.email,
* password: TextDecoder.varchar(5, 50),
* passwordRepeat: TextDecoder.varchar(5, 50)
* }),
* ObjectDecoder.guard(user => {
* return user.password === user.passwordRepeat
* ? undefined
* : DecodeError.object([
* DecodeError.key('passwordRepeat', DecodeError.value(user.passwordRepeat, `Password does not match password confirmation`))
* ])
* })
* )
*
* const input: unknown = {
* email: 'test@example.com',
* password: '12345',
* passwordRepeat: '12345'
* }
*
* expect(pipe(input, Decoder.validate(SignupDto), Result.isKo)).toBe(true)
* ```
*/
guard: <I, O extends Dict<any>>(fn: (input: O) => Option<DecodeError.Value | DecodeError.ObjectLike>) => (decoder: ObjectDecoder<I, O>) => ObjectDecoder<I, O>;
/**
* @description
* Merge multiple `ObjectDecoder`s.
* If a property has already been declared, the decoder for this property will be overwritten.
*
* @example
* ```ts
* const A = ObjectDecoder.struct({
* a: TextDecoder.string,
* ab: TextDecoder.string
* })
* const B = ObjectDecoder.struct({
* ab: NumberDecoder.number,
* b: NumberDecoder.number
* })
* const C = ObjectDecoder.merge(A, B)
*
* interface C extends Decoder.TypeOf<typeof C> {}
*
* // Interface C is now equals to:
* type C = {
* a: string
* ab: number
* b: number
* }
*
* // Note that "ab: TextDecoder.string" has been overwritten and will not be executed
* ```
*/
merge: typeof merge;
/**
* @description
* By default, `ObjectDecoder.struct` will skip and ignore extra properties.
* If you wish to keep extra properties (all properties not validated by the struct), you can use this util.
*
* @example
* ```ts
* const Response = pipe(
* ObjectDecoder.struct({
* status: EnumDecoder.literal('OK', 'KO'),
* message: TextDecoder.string,
* }),
* ObjectDecoder.additionalProperties
* )
* ```
*/
additionalProperties: <I_1, O_1 extends Dict<any>>(decoder: ObjectDecoder<I_1, O_1>) => Decoder<I_1, O_1 & Dict<any>>;
/**
* @description
* Execute a specific decoder depending on the value of a given "type" property.
*
* @example
* ```ts
* const Geom = ObjectDecoder.sum('type', {
* Circle: struct({
* radius: NumberDecoder.number
* }),
* Rectangle: struct({
* width: NumberDecoder.number,
* height: NumberDecoder.number
* })
* })
*
* type Geom = Decoder.TypeOf<typeof Geom>
* const geom: Geom = {
* type: 'Rectangle',
* height: 5
* }
* ```
*/
sum: typeof sum;
};
declare type ArrayDecoder<I, O extends any[]> = Decoder<I, O>;
/**
* @namespace ArrayDecoder
*
* @description
* This namespace contains array decoders and additional utilities for array validations.
*/
declare const ArrayDecoder: {
/**
* @description
* Check if the input is an array.
*/
unknownArray: Decoder<unknown, unknown[]>;
/**
* @description
* Check if the input is an array of a given type.
*/
array: <A>(decoder: Decoder<unknown, A>) => ArrayDecoder<unknown, A[]>;
/**
* @description
* Check if the input is a non empty array of a given type.
*/
nonEmptyArray: <O>(decoder: Decoder<unknown, O>) => Decoder<unknown, _apoyo_std.NonEmptyArray<O>>;
/**
* @description
* Check the length of the string
*/
length: (len: number) => <D extends ArrayDecoder<any, any>>(value: D) => D;
/**
* @description
* Check the minimum length of the array
*
* @example
* ```ts
* const decoder = pipe(
* ArrayDecoder.array(NumberDecoder.number),
* ArrayDecoder.min(1)
* )
*
* expect(pipe([1], Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe([], Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
min: (minLength: number) => <D_1 extends ArrayDecoder<any, any>>(value: D_1) => D_1;
/**
* @description
* Check the maximum length of the array
*
* @example
* ```ts
* const decoder = pipe(
* ArrayDecoder.array(NumberDecoder.number),
* ArrayDecoder.max(5)
* )
*
* expect(pipe([1,2,3,4,5], Decoder.validate(decoder), Result.isOk)).toBe(true)
* expect(pipe([1,2,3,4,5,6], Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
max: (maxLength: number) => <D_2 extends ArrayDecoder<any, any>>(value: D_2) => D_2;
/**
* @description
* Check both the minimum and maximum length of the array
*
* @example
* ```ts
* const decoder = pipe(
* ArrayDecoder.array(NumberDecoder.number),
* ArrayDecoder.between(1, 5)
* )
*
* expect(pipe([], Decoder.validate(decoder), Result.isKo)).toBe(true)
* ```
*/
between: (minLength: number, maxLength: number) => <D_1 extends ArrayDecoder<any, any>>(value: D_1) => D_1;
};
declare type DateDecoder<I, O extends Date> = Decoder<I, O>;
/**
* @namespace DateDecoder
*
* @description
* This namespace contains date decoders and additional utilities for date validations.
*/
declare const DateDecoder: {
/**
* @description
* Check if the input is a date.
*
* @example
* ```ts
* expect(pipe('2021-01-01', Decoder.validate(DateDecoder.date), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01 12:00:00', Decoder.validate(DateDecoder.date), Result.isKo)).toBe(true)
* ```
*/
date: Decoder<unknown, Date>;
/**
* @description
* Check if the input is a datetime.
* @example
* ```ts
* expect(pipe('2021-01-01', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01 12:00:00', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01 12:00:00Z', Decoder.validate(DateDecoder.datetime), Result.isOk)).toBe(true)
* ```
*/
datetime: Decoder<unknown, Date>;
/**
* @description
* Check if the input is a valid `Date` object.
*
* @example
* ```ts
* expect(pipe(new Date('2021-01-01'), Decoder.validate(DateDecoder.strict), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01', Decoder.validate(DateDecoder.strict), Result.isKo)).toBe(true)
* ```
*/
strict: Decoder<unknown, Date>;
/**
* @description
* Check if the input can be cast to a valid `Date` object.
*
* expect(pipe(new Date('2021-01-01'), Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01', Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true)
* expect(pipe('2021-01-01 12:00:00Z', Decoder.validate(DateDecoder.native), Result.isOk)).toBe(true)
*/
native: Decoder<unknown, Date>;
/**
* @description
* Check if the date is above a specific date
*
* @example
* ```ts
* // The date needs to be above the current date
* const futureDate = pipe(
* Decoder.date,
* Decoder.min(() => {
* const today = new Date().toISOString().split('T')[0]
* return new Date(today)
* })
* )
* ```
*/
min: (minDate: Date | (() => Date)) => <A>(decoder: Decoder<A, Date>) => Decoder<A,