Hey,
I have the following function which validates a contract and returns a model off of it. I would like to return an informative error string in case the conversion failed due to unexpected values in the contract.
Currently I have the following:
type Color = 'Red' | 'Green' | 'Blue'
interface PersonContract {
name: string
favoriteColor: string
}
interface Person {
name: string,
favoriteColor: Color
}
function personFromContract(contract: PersonContract): Either<string, Person> {
return fromNullable<string>('No contract was provided')(contract)
.chain(person => {
if (person.favoriteColor !== 'Red' &&
person.favoriteColor !== 'Green' &&
person.favoriteColor !== 'Blue') {
return left(`Unexpected color in contract: ${person.favoriteColor}`)
}
return right<string, Person>({
name: person.name,
favoriteColor: person.favoriteColor
})
})
}
But I feel that the following would be a more appropriate use of Either's functions.
function personFromContract(contract: PersonContract): Either<string, Person> {
return fromNullable('No contract was provided')(contract)
.filterOrElse(person => !(person.favoriteColor !== 'Red' &&
person.favoriteColor !== 'Green' &&
person.favoriteColor !== 'Blue'),
`Unexpected color in contract: no color to refer to :(`)
.map<Person>(person => ({
name: person.name,
favoriteColor: person.favoriteColor as Color
}))
}
Is there a way to avoid the explicit if and return right/left in the first piece of code and still keep the value available for the error string? It kind of reminds me RxJs where the Observable has a do operator for side effects, where you could stash a temporary value in the closure and then use it later, but I'm not sure I like that approach if there's a more functional approach.
Thanks,
Boaz
I would recommend io-ts for this job. It is not only useful for validation of input/output data it can also be used to transform/validate data of similar shape.
So your code can be reduced to
const Person = t.interface({
name: t.string,
favoriteColor: t.keyof({ Red: true, Green: true, Blue: true })
});
type Person = t.TypeOf<typeof Person>;
const personFromContract = (contract: PersonContract): Either<string, Person> =>
Person.decode(contract).mapLeft(() => `Unexpected color in contract: ${contract.favoriteColor}`);
You may use keyof instead of union, better error message and way faster
@sledorze thanks for the hint I adjusted my code.
@boaz-chen I would also recommend to active strict mode in your typescript project, cause your null-check at the start of the personFromContract Function should already be catched by tsc.
@boaz-chen speaking about validations, the functional way, when possible, is using "applicative validation" (<= search for this term to get some tutorials)
// curried constructor for Person
const person = (name: string) => (favoriteColor: Color): Person => ({ name, favoriteColor })
// validate each field
const validateName = (s: string): Either<string, string> => right(s)
const isColor = (s: string): s is Color => s !== 'Red' && s !== 'Green' && s !== 'Blue'
const validateColor = (s: string): Either<string, Color> =>
isColor(s) ? right(s) : left(`Unexpected color in contract: ${s}`)
// validate all
function validatePersonContract(contract: PersonContract): Either<string, Person> {
const fa = validateName(contract.name)
const fb = validateColor(contract.favoriteColor)
// lifting `person`
return fb.ap(fa.map(person))
}
Thanks for the great info guys.
FYI I'm writing a PR which adds a fromRefinement function to Either in order to simplify the definition of validateColor
// const validateColor: (a: string) => Either<string, Color>
const validateColor = fromRefinement(isColor, s => `Unexpected color in contract: ${s}`)
Most helpful comment
FYI I'm writing a PR which adds a
fromRefinementfunction toEitherin order to simplify the definition ofvalidateColor