Skip to content

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 | ArkErrorsout = parseJson('{ "ark": "type" }') // ArkErrors: must be a string (was object) const const badOut: object | ArkErrorsbadOut = parseJson(const out: object | ArkErrorsout)

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 | ArkErrorsbadOut = 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 piped 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 | ArkErrorsbadOut = parseJson('{ unquoted: "keys" }') const const out: object | ArkErrorsout = parseJson('{ "ark": "type" }') if (const out: object | ArkErrorsout instanceof type.errors) const out: ArkErrorsout.throw() // Unfortunately, a validated `object` still isn't very useful... else console.log(const out: objectout)

The best part about pipe is that since any Type is root-invokable, Types 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.semver;
}>>
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.semver;
}, {}>>(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 & {
    " arkConstrained": Branded<"json">;
}) => To<{
    name: string;
    version: string.semver;
}>>
parseJson
=
const type: TypeParser
<"string.json.parse", Type<(In: string.json) => To<object>, {}>>(def: "string.json.parse") => Type<(In: string.json) => To<object>, {}> (+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: RuntimeErrorsout.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.