gcanti / io-ts-types

A collection of codecs and combinators for use with io-ts
https://gcanti.github.io/io-ts-types/
MIT License
311 stars 40 forks source link

How to encode a blank string to null or undefined? #145

Closed efenderbosch closed 4 years ago

efenderbosch commented 4 years ago

Is there a simple extension, or do I need a completely new type?

mlegenhausen commented 4 years ago

What Type do you want to express? If you want something like Type<Option<NonEmptyString>, string | null> you could do optionFromNullable(nonEmptyString).

efenderbosch commented 4 years ago

I want to avoid form submission that ends up with JSON like:

{
  "foo": "someValue",
  "bar": ""
}

being sent to the server.

What should be sent instead is:

{
  "foo": "someValue"
}

Server-side validation is enforcing an if-present-must-not-be-blank rule. However, simply tabbing through the input on the form and hitting submit is sending the empty string.

Right now I have a few custom "types", including:

export const nullAsUndefined = fromNullable(iots.undefined, undefined)
export const OptionalString = iots.union([iots.string, nullAsUndefined])

Many of my "strings" on the models are OptionalString since I want deserialization of a missing attribute to be undefined instead of null.

efenderbosch commented 4 years ago

I need the empty string -> undefined for POST, but I also need empty string -> null for PATCH.

Pretty standard entity like this:

export const Foo = iots.interface({
  id: UUID,
  name: iots.string,
  description: iots.union([iots.string, iots.undefined]),
  created: DateFromISOString,
  updated: DateFromISOString
})
export type Foo = iots.TypeOf<typeof Foo>

export interface FooPatch {
  name?: string;
  description?: string;
}

export const toFooPatch = (current: Foo, toUpdate: Foo): FooPatch => ({
  name: current.name === toUpdate.name ? undefined : toUpdate.name,
  description: current.description === toUpdate.description ? undefined : toUpdate.description
})

The JSON PATCH for an empty string for description from the UI should result look like:

{
  "description": null
}

instead of:

{
  "description": ""
}
efenderbosch commented 4 years ago

I think mapToOutput is what I want. First pass, this looks like it is working how I want:

const emptyStringToUndefined = (s?: string): string | undefined => s?.trim()?.length === 0 ? undefined : s
const emptyStringToNull = (s?: string): string | null | undefined =>
  s === undefined ? undefined : s?.trim()?.length === 0 ? null : s

export const EmptyStringAsUndefined = mapOutput(iots.string, emptyStringToUndefined)
export type EmptyStringAsUndefined = iots.TypeOf<typeof EmptyStringAsUndefined>
export const EmptyStringAsNull = mapOutput(iots.string, emptyStringToNull)
export type EmptyStringAsNull = iots.TypeOf<typeof EmptyStringAsNull>