Skip to content

Custom Parsers and Guards

The API of PureParse is designed to be as simple as possible, so that it is easy to extend the functionality.

NOTE

Before writing a custom parser, consider whetehr you can apply a simple transformation on top of a built-in parser. See the Transformations guide for more information.

Custom Parsers

To create a custom parser, implement a function of the Parser<?> type. For example, to create a parser that parses a stringified number from a number:

ts
import {
  Parser,
  ParseResult,
  success,
  failure,
  isString,
  parseNumber,
} from 'pure-parse'

const parseStringifiedNumber: Parser<number> = (data: unknown) => {
  const result = parseNumber(data)
  if (data.error) {
    return result
  }
  return success(result.value.toString())
}

TIP

The success and failure functions are simple constructors for the ParseResult type and make the code more readable.

Very often though, it's better to use map or chain:

ts
const parseStringified = map(parseNumber, (it) => it.toString())

Higher-order Parsers

To create a custom, higher-order parser function, create a generic function that accepts a parser as argument and returns a new parser. Here is a working example with generic trees:

ts
import { Parser, union, object, equals, array } from 'pure-parse'

type Leaf<T> = {
  tag: 'leaf'
  data: T
}
type Tree<T> = {
  tag: 'tree'
  data: (Tree<T> | Leaf<T>)[]
}

// `RequiredParser` means that the user gets an error if they pass an optional parser; for example, `leaf(optional(parseString))`
const leaf =
  <T>(parser: RequiredParser<T>): Parser<Leaf<T>> =>
  (data) =>
    // @ts-expect-error TypeScript gives a false error for the `data` property:
    //  `RequiredParser` guarantees that `parser` does not represent an optional property, yet TypeScript complains
    object({
      tag: equals('leaf'),
      data: parser,
    })(data)

const tree =
  <T>(parser: RequiredParser<T>): Parser<Tree<T>> =>
  (data) =>
    object({
      tag: equals('tree'),
      data: array(oneOf(leaf(parser), tree(parser))),
    })(data)

which will parse (and infer the type of) the following data:

ts
const myTree: Tree = {
  tag: 'tree',
  data: [
    {
      tag: 'leaf',
      data: 'package.json',
    },
    {
      tag: 'tree',
      data: [
        {
          tag: 'leaf',
          data: 'index.ts',
        },
      ],
    },
  ],
}

const parseTree = tree(isString)

Higher-order Guards

To do the same with a validator, simply import the guard functions as alias them:

ts
import {
  Guard,
  equalsGuard as equals,
  objectGuard as object,
  unionGuard as union,
  arrayGuard as arrays,
} from 'pure-parse'

The rest of the code becomes the same as if you were implementing a parser:

ts
type Leaf<T> = { tag: 'leaf'; data: T }
type Tree<T> = {
  tag: 'tree'
  data: (Tree<T> | Leaf<T>)[]
}

const leafGuard =
  <T>(guard: Guard<T>): Guard<Leaf<T>> =>
  // @ts-expect-error TypeScript gives a false error for the `data` property:
  //  `RequiredGuard` guarantees that `parser` does not represent an optional property, yet TypeScript complains
  (data) =>
    objectGuard({
      tag: equalsGuard('leaf'),
      data: guard,
    })(data)

const treeGuard =
  <T>(guard: Guard<T>): Guard<Tree<T>> =>
  (data) =>
    objectGuard({
      tag: equalsGuard('tree'),
      data: arrayGuard(unionGuard(leafGuard(guard), treeGuard(guard))),
    })(data)

This will validate (and infer the type of) the following data:

ts
const myTree: Tree = {
  tag: 'tree',
  data: [
    {
      tag: 'leaf',
      data: 'package.json',
    },
    {
      tag: 'tree',
      data: [
        {
          tag: 'leaf',
          data: 'index.ts',
        },
      ],
    },
  ],
}

const isTree = tree(isString)