Skip to content

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

Closed
gyzerok opened this issue Mar 13, 2018 · 13 comments
Closed

Can work done for this library be reused for form validation? #148

gyzerok opened this issue Mar 13, 2018 · 13 comments
Milestone

Comments

@gyzerok
Copy link

gyzerok commented Mar 13, 2018

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:

interface Form {
  foo : string | null,
  bar : number,
}

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:

interface Body {
  foo : string,
  bar : number,
}

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:

interface Model {
  foo : string | null,
  bar : number,
}

interface ValidatedModel {
  foo : string,
  bar : number,
}

validate(model, t.interface({
  foo: [t.defined, 'foo have to be selected'],
  bar: t.keep,
}))

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?

@gcanti
Copy link
Owner

gcanti commented Mar 13, 2018

Let me understand, io-ts is able to decode any value coming from a form

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
})
*/

I am not sure about what would be a good API on in TypeScript land for this idea

so are you looking for a way to configure the errors?

@gyzerok
Copy link
Author

gyzerok commented Mar 13, 2018

Hm, you are right, even without things like keep it will work just fine.

so are you looking for a way to configure the errors?

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: []
})
*/

@volkanunsal
Copy link

@gyzerok It sounds like you're talking about a reporter. That one only outputs the paths, but there could be one that returns a Validation type.

@gyzerok
Copy link
Author

gyzerok commented Mar 13, 2018

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.

@gcanti
Copy link
Owner

gcanti commented Mar 13, 2018

In general you could define a function for mapLeft containing the custom logic

// 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"])

@gcanti
Copy link
Owner

gcanti commented Mar 13, 2018

...maybe along with a withMessage combinator (it's just a POC so take it with a pinch of salt)

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"])
*/

@gyzerok
Copy link
Author

gyzerok commented Mar 14, 2018

@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.

@gyzerok
Copy link
Author

gyzerok commented Apr 3, 2018

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.

@gyzerok gyzerok closed this as completed Apr 3, 2018
@spacejack
Copy link

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.

@gcanti
Copy link
Owner

gcanti commented Jan 18, 2019

@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) message field to ValidationError, so

export interface ValidationError {
  readonly value: unknown
  readonly context: Context,
  readonly message?: string
}

This would make easier to write the withMessage combinator and reporters (like PathReporter) could make good use of the new message field

@gcanti gcanti reopened this Jan 18, 2019
@gcanti gcanti added this to the 1.7 milestone Jan 18, 2019
@gcanti
Copy link
Owner

gcanti commented Jan 18, 2019

Example

Given this change in PathReporter

function getMessage(e: ValidationError): string {
  return e.message !== undefined
    ? e.message
    : `Invalid value ${stringify(e.value)} supplied to ${getContextPath(e.context)}`
}

and the following withMessage definition

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' ]
*/

@livingmine
Copy link

livingmine commented Jan 19, 2019

@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.

@gcanti
Copy link
Owner

gcanti commented Jan 19, 2019

provide the error message(s) from inside the validate

@livingmine you can do that

Example

First of all for convenience we could also add a message argument to failure

export const failure = <T>(value: unknown, context: Context, message?: string): Validation<T> =>
  failures([({ value, context, message })])

You can define a Name codec containing the validation logic + specific messages

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 message field to ValidationError opens up many possibilities and people will be able to write combinators like withMessage, withRefinementMessage, etc... and then leverage those messages in their custom reporters

gcanti added a commit that referenced this issue Jan 23, 2019
- add optional message field to ValidationError
- add message argument to failure
- PathReporter should account for the new field
gcanti added a commit that referenced this issue Jan 23, 2019
- add optional message field to ValidationError
- add message argument to failure
- PathReporter should account for the new field
gcanti added a commit that referenced this issue Jan 23, 2019
- add optional message field to ValidationError
- add message argument to failure
- PathReporter should account for the new field
@gcanti gcanti closed this as completed in f73be52 Jan 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants