Type safe programming is often broken on the edge to the real world. Typescript does not come with any batteries to mitigate this. A cleverly designed library called io-ts fixes this problem. This is how I use it:

A generic parser function:

import { pipe } from "fp-ts/lib/function"
import { fold } from 'fp-ts/lib/Either'
import { Decoder, Errors } from "io-ts"

export const parse = <A>(codec: Decoder<any, A>, value: any): A => {
  // failure handler
  const onLeft = (errors: Errors) => {
    throw new Error(`Could not parse ${JSON.stringify(errors)}`)
  }

  return pipe(codec.decode(value), fold(onLeft, v => v))
}

A quick example of rewriting typescript types into their equivalent io-ts definitions goes as follows:

Before:

type User = {
  name: string
  uid: string
  description?: string | undefined
  metadata: {
    [m: string]: string
  }
  gender: "male" | "female" | "other"
}

After:

const User = intersection([
  type({
    name: string,
    uid: string,
    metadata: record(string, string),
    gender: union([literal("male"), literal("female"), literal("other")])
  }),
  partial({
    description: string
  })
])
type User = TypeOf<typeof User>

And lastly we do type safe decoding as follows:

try {
  const u = parse(User, ...)
  ...
} catch (e) {
  console.error("Could not parse")
}

Couple of notes:

  • We overload the name such that it can both be used as a type and as a decoder
  • The types of Usr and User are mutually assignable. This means that not changes will have to be made to the rest of the application.
  • The resulting variable u is correctly typed.