-
Notifications
You must be signed in to change notification settings - Fork 328
Can work done for this library be reused for form validation? #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Let me understand, import * as t from 'io-ts'
import { IntegerFromString } from 'io-ts-types'
interface FormModel {
name: string | null
age: string // likeliy this is a string in the UI
}
const ValidatedModel = t.type({
name: t.string,
age: IntegerFromString
})
// let's say this comes from a form
const formState: FormModel = {
name: 'Giulio',
age: '44'
}
console.log(ValidatedModel.decode(formState))
/*
right({
"name": "Giulio",
"age": 44 // integer
})
*/
so are you looking for a way to configure the errors? |
Hm, you are right, even without things like
Yeah, ultimately this is what I am looking for. Plus some sensible format of getting them after validation. For example in Elm library I've brought as an example they come as array. In TypeScript it could be the same or it can be repeating shape of the record. interface Model {
foo : string | null,
bar : number,
}
interface ValidatedModel {
foo : string,
bar : number,
}
const model = {
foo: null,
bar: 1
};
validate(model, t.interface({
foo: [t.string, 'foo have to be selected'],
bar: t.number,
})
/*
left([
'foo have to be selected'
])
or
left({
foo: ['foo have to be selected'],
bar: []
})
*/ |
Yeah, I guess the format errors are getting reported could be configured with different type of reporter. However I am not sure if ability to specify custom errors do depend on reporter. |
In general you could define a function for // dummy handler
const getErrorMessage = (e: t.ValidationError): string => {
const key = e.context[1].key
if (key === 'name') {
return 'Invalid name'
} else {
return 'Invalid age'
}
}
console.log(
ValidatedModel.decode({
name: null,
age: 'foo'
}).mapLeft(errors => errors.map(getErrorMessage))
)
// left(["Invalid name", "Invalid age"]) |
...maybe along with a interface CustomValidationError extends t.ValidationError {
message?: string
}
const withMessage = <A, O>(type: t.Type<A, O, t.mixed>, message: string): t.Type<A, O, t.mixed> => {
return new t.Type(
type.name,
type.is,
(m, c) =>
type.validate(m, c).mapLeft(es => es.map((e: CustomValidationError) => (e.message ? e : { ...e, message }))),
type.encode
)
}
const getErrorMessage = (e: CustomValidationError) => e.message || `Invalid field`
const Name = withMessage(t.string, 'Invalid name')
const Age = withMessage(
t.refinement(withMessage(t.number, 'Age is not a number'), n => n % 1 === 0),
'Age is not an integer'
)
const Foo = withMessage(
t.type({
bar: withMessage(t.string, 'Invalid bar')
}),
'Invalid foo'
)
console.log(
t
.type({
name: Name,
age: Age,
foo: Foo
})
.decode({
name: null,
age: 'a', // 1.2
foo: null
})
.mapLeft(errors => errors.map(getErrorMessage))
)
/*
left(["Invalid name", "Age is not a number", "Invalid foo"])
*/ |
@gcanti looks really promising, thanks! I will try to use it and come back with how things went. Let's keep the issue open for now if you don't mind. |
Currently situation changed so, that I don't need to do such validation in TypeScript. So I can't really test the proposal and give any feedback. I'l close issue for now and reopen it later if needed. |
Just commenting to say that the info in this closed issue is quite useful. I was wondering how to present user-friendly validation errors. A summary with examples in this thread would be nice to have on the main readme. |
@spacejack not sure if this still relevant to you but I'm thinking of better supporting custom error messages by adding an optional (for backward compatibility) export interface ValidationError {
readonly value: unknown
readonly context: Context,
readonly message?: string
} This would make easier to write the |
Example Given this change in function getMessage(e: ValidationError): string {
return e.message !== undefined
? e.message
: `Invalid value ${stringify(e.value)} supplied to ${getContextPath(e.context)}`
} and the following const clone = <T>(t: T): T => {
const r = Object.create(Object.getPrototypeOf(t))
Object.assign(r, t)
return r
}
const withValidate = <C extends t.Any>(codec: C, validate: C['validate'], name: string = codec.name): C => {
const r: any = clone(codec)
r.validate = validate
r.name = name
return r
}
const withMessage = <C extends t.Mixed>(codec: C, message: string): C => {
return withValidate(codec, (i, c) =>
codec.validate(i, c).mapLeft(() => [
{
value: i,
context: c,
message
}
])
)
} you can customize a codec const Person = t.type({
name: withMessage(t.string, 'Invalid name, enter a string'),
age: withMessage(t.number, 'Invalid age, enter a number')
})
console.log(PathReporter.report(Person.decode({})))
/*
[ 'Invalid name, enter a string',
'Invalid age, enter a number' ]
*/ |
@gcanti Don't you think it's better to somehow provide the error message(s) from inside the validate instead, so that the error message(s) can be generated based on some validation logic? For example, an invalid name can be replaced by a more specific message(s) such as name is too short, only lowercase is allowed, etc. or the combination of them. |
@livingmine you can do that Example First of all for convenience we could also add a export const failure = <T>(value: unknown, context: Context, message?: string): Validation<T> =>
failures([({ value, context, message })]) You can define a const Name = new t.Type(
'Name',
(u): u is string => t.string.is(u) && u.length >= 5 && u.toLocaleLowerCase() === u,
(u, c) => {
if (!t.string.is(u)) {
return t.failure(u, c, 'Invalid name, enter a string')
} else if (u.length < 5) {
return t.failure(u, c, 'Invalid name, too short')
} else if (u.toLocaleLowerCase() !== u) {
return t.failure(u, c, 'Invalid name, only lowercase is allowed')
} else {
return t.success(u)
}
},
t.string.encode
)
const Person = t.type({
name: Name,
age: t.number
})
console.log(PathReporter.report(Person.decode({ name: 'foo', age: 45 })))
// [ 'Invalid name, too short' ]
console.log(PathReporter.report(Person.decode({ name: 'AAAAA', age: 45 })))
// [ 'Invalid name, only lowercase is allowed' ] or with much less boilerplate // helper combinator
export const withRefinementMessage = <A, O, I>(
name: string,
codec: t.Type<A, O, I>,
getMessage: (a: A) => string | undefined
): t.Type<A, O, I> => {
return new t.Type(
name,
(u): u is A => codec.is(u) && getMessage(u) === undefined,
(i, c) =>
codec.validate(i, c).chain(a => {
const message = getMessage(a)
return message === undefined ? t.success(a) : t.failure(a, c, message)
}),
codec.encode
)
} const Name = withRefinementMessage('Name', withMessage(t.string, 'Invalid name, enter a string'), s => {
if (s.length < 5) {
return 'Invalid name, too short'
} else if (s.toLocaleLowerCase() !== s) {
return 'Invalid name, only lowercase is allowed'
}
})
const Person = t.type({
name: Name,
age: t.number
})
console.log(PathReporter.report(Person.decode({ name: 'foo', age: 45 })))
// [ 'Invalid name, too short' ]
console.log(PathReporter.report(Person.decode({ name: 'AAAAA', age: 45 })))
// [ 'Invalid name, only lowercase is allowed' ] Now this is just a quick POC, but the point is that adding the |
- add optional message field to ValidationError - add message argument to failure - PathReporter should account for the new field
- add optional message field to ValidationError - add message argument to failure - PathReporter should account for the new field
- add optional message field to ValidationError - add message argument to failure - PathReporter should account for the new field
First of all thanks a lot for continuous work on this library! I was and am using it in different projects from almost very first version and extremely happy with it 😃
My codebase currently consists of both TypeScript and Elm. Recently I was working on live form validation in my forms. While working I've discovered following use case.
Let's say we have a model within the form:
Here let's say
foo
in the model is something not selected by default, but it is required field. The example of this might be dropdown with empty value by default.At the same time backend expects from us data of following shape to be sent:
So during validation we need to ensure that
foo
in fact was selected.While investigating this use case I've stumbled upon elm-verify library which uses same idea as parsing JSON for form validation. It seems like pretty close use case as
io-ts
has with the difference of errors being configurable.I am not sure about what would be a good API on in TypeScript land for this idea, but it can be something like this:
Since the functionality is quite close to what
io-ts
already does I was thinking that it might make sense to reuse some parts of the work you've already done.What do you think? Would you be interested in creating such library?
The text was updated successfully, but these errors were encountered: