Morphs & More
Sometimes, data at the boundaries of your code requires more than validation before it’s ready to use.
Morphs allow you to arbitrarily transform the shape and format of your data.
Morphs can be piped before, after or between validators and even chained to other morphs.
// Hover to see the type-level representation
const const parseJson: Type<(In: string) => Out<object>>
parseJson = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").pipe((s): object => JSON.parse(s))
// object: { ark: "type" }
const const out: object | ArkErrors
out = parseJson('{ "ark": "type" }')
// ArkErrors: must be a string (was object)
const const badOut: object | ArkErrors
badOut = parseJson(const out: object | ArkErrors
out)
This is a good start, but there are still a couple major issues with our morph.
What happens if we pass a string that isn’t valid JSON?
// Uncaught SyntaxError: Expected property name ☠️
const const badOut: object | ArkErrors
badOut = parseJson('{ unquoted: "keys" }')
Despite what JSON.parse
might have you believe, throwing exceptions and returning any
are not very good ways to parse a string. By default, ArkType assumes that if one of your morphs or narrows throws, you intend to crash.
If you do happen to find yourself at the mercy of an unsafe API, you might consider wrapping your function body in a try...catch
.
Luckily, there is a built-in API for wrapping pipe
d functions you don’t trust:
const const parseJson: Type<(In: string) => Out<object>>
parseJson = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").pipe.try((s): object => JSON.parse(s))
// Now returns an introspectable error instead of crashing 🎉
const const badOut: object | ArkErrors
badOut = parseJson('{ unquoted: "keys" }')
const const out: object | ArkErrors
out = parseJson('{ "ark": "type" }')
if (const out: object | ArkErrors
out instanceof type.errors) const out: ArkErrors
out.throw()
// Unfortunately, a validated `object` still isn't very useful...
else console.log(const out: object
out)
The best part about pipe
is that since any Type
is root-invokable, Type
s themselves are already morphs! This means validating out parsed output is as easy as adding another pipe:
const const parseJson: Type<(In: string) => To<{
name: string;
version: string;
}>>
parseJson = const type: TypeParser
<"string", Type<string, {}>>(def: "string") => Type<string, {}> (+2 overloads)
type("string").pipe.try(
(s): object => JSON.parse(s),
const type: TypeParser
<{
readonly name: "string";
readonly version: "string.semver";
}, Type<{
name: string;
version: string;
}, {}>>(def: validateObjectLiteral<{
readonly name: "string";
readonly version: "string.semver";
}, {}, bindThis<...>>) => Type<...> (+2 overloads)
type({
name: "string",
version: "string.semver"
})
)
const const out: ArkErrors | {
name: string;
version: string;
}
out = parseJson('{ "name": "arktype", "version": "2.0.0" }')
if (!(const out: ArkErrors | {
name: string;
version: string;
}
out instanceof type.errors)) {
// Logs "arktype:2.0.0"
console.log(`${const out: {
name: string;
version: string;
}
out.name}:${const out: {
name: string;
version: string;
}
out.version}`)
}
At this point, our implementation is starting to look pretty clean, but in many cases like this one, we can skip straight to the punch line with one of ArkType’s many built-in aliases for validation and parsing, string.json.parse
:
// .to is a sugared .pipe for a single parsed output validator
const const parseJson: Type<(In: string) => To<{
name: string;
version: string;
}>>
parseJson = const type: TypeParser
<"string.json.parse", Type<(In: string) => To<Json>, {}>>(def: "string.json.parse") => Type<(In: string) => To<Json>, {}> (+2 overloads)
type("string.json.parse").to({
name: "string",
version: "string.semver"
})
const const out: ArkErrors | {
name: string;
version: string;
}
out = parseJson('{ "name": true, "version": "v2.0.0" }')
if (const out: ArkErrors | {
name: string;
version: string;
}
out instanceof type.errors) {
// hover out.summary to see the default error message
console.error(const out: RuntimeErrors
out.RuntimeErrors.summary: string
name must be a string (was true)
version must be a semantic version (see https://semver.org/) (was "v2.0.0")summary)
}
If you’ve made it this far, congratulations! You should have all the fundamental intuitions you need to bring your types to runtime ⛵
Our remaining docs will help you understand the trade offs between ArkType’s most important APIs so that no matter the application, you can find a solution that feels great to write, great to read, and great to run.