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:
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
:
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:
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:
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:
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:
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:
const myTree: Tree = {
tag: 'tree',
data: [
{
tag: 'leaf',
data: 'package.json',
},
{
tag: 'tree',
data: [
{
tag: 'leaf',
data: 'index.ts',
},
],
},
],
}
const isTree = tree(isString)