type Validated = ValidatedOk | ValidatedError type ErrorOfValidated> = V['errorType'] type ValueOfValidated> = V['valueType'] const Validated = { ok(a: A): ValidatedOk { return new ValidatedOk(a) }, error(error: E): ValidatedError { return new ValidatedError([error]) }, combine }>(o: O): Validated, { [K in keyof O]: ValueOfValidated }> { const errors: any[] = [] const values: { [K in keyof O]?: any } = {} Object.keys(o).forEach(key => { const validated = o[key] if (validated.kind === ValidatedOk.kind) { values[key] = validated.value } else if (validated.kind === ValidatedError.kind) { validated.errors.forEach((e: any) => { errors.push(e) }) } else { const exhaustive: never = validated throw exhaustive } }) if (errors.length > 0) { return new ValidatedError(errors) } else { return new ValidatedOk(values as any) } } } class ValidatedOk { static readonly kind = 'ok' readonly kind = ValidatedOk.kind readonly valueType: A = null as A readonly errorType: E = null as E constructor(readonly value: A) { } map(fn: (a: A) => B): Validated { return new ValidatedOk(fn(this.value)) } fold(ok: (v: ValidatedOk) => B, error: (v: ValidatedError) => B): B { return ok(this) } } class ValidatedError { static readonly kind = 'error' readonly kind = ValidatedError.kind readonly valueType: A = null as A readonly errorType: E = null as E constructor(readonly errors: E[]) { } map(fn: (a: A) => B): Validated { return new ValidatedError(this.errors) } fold(ok: (v: ValidatedOk) => B, error: (v: ValidatedError) => B): B { return error(this) } coerceValue(): ValidatedError { return new ValidatedError(this.errors) } } class ValidationRule { readonly sourceType: A = null as A readonly targetType: B = null as B readonly errorType: E = null as E constructor(readonly rule: (a: A) => Validated) { } followedBy(rule: (b: B) => Validated): ValidationRule { const composedRule = (a: A) => this.rule(a).fold( ok => rule(ok.value), err => err.coerceValue() ) return new ValidationRule(composedRule) } static fromPredicate(predicate: (a: A) => boolean, error: E): ValidationRule { return new ValidationRule((a: A) => predicate(a) ? new ValidatedOk(a) : new ValidatedError([error]) ) } static combine }>(o: O): ValidationRule, {[K in keyof O]: SourceOfValidationRule }, {[K in keyof O]: TargetOfValidationRule }> { const rule = a => { const validatedsToCombine: { [K in keyof O]?: Validated } = {} Object.keys(o).forEach(key => { validatedsToCombine[key] = o[key].rule(a[key]) }) return Validated.combine(validatedsToCombine) } return new ValidationRule(rule) } } type SourceOfValidationRule> = V['sourceType'] type TargetOfValidationRule> = V['targetType'] type ErrorOfValidationRule> = V['errorType'] const isBiggerThanTwo = new ValidationRule(n => n > 2 ? Validated.ok('Big ' + n) : Validated.error('not.bigger.than.two') ) const isFalse = ValidationRule.fromPredicate(b => b === false, 'not.false') const rules = { n: isBiggerThanTwo, b: isFalse } const rulesV = ValidationRule.combine(rules) console.log(rulesV.rule({ n: 4, b: true })) console.log(rulesV.rule({ n: 4, b: false })) console.log(rulesV.rule({ n: 1, b: true }))