Using ArkType for TypeScript runtime validation
Tags: typescript, webdev
Published: 2023-05-30 15:12:37

**Written by [Yan Sun](https://blog.logrocket.com/author/yansun/)✏️**

TypeScript provides robust features for type checking at compile time, but it doesn’t have inbuilt runtime type checking because the types don’t exist at runtime. As a result, runtime errors can occur due to unexpected input data. 

That’s where ArkType comes into play. ArkType is a library that provides runtime validation for TypeScript interfaces and classes. It can catch errors caused by unexpected data at runtime, while still leveraging the static type system at compile time. 

In this blog post, we’ll walk through how to use ArkType for runtime validation with TypeScript. 

_Jump ahead:_

*   [What is ArkType?](#what-is-arktype)
    *   [How ArkType works](#how-arktype-works)
    *   [Benefits of using ArkType](#benefits-using-arktype)
*   [Setting up and using ArkType](#setting-up-using-arktype)
    *   [ArkType type definitions](#arktype-type-definitions)
*   [Using scopes in ArkType](#using-scopes-arktype)
*   [Automatically discriminating discriminated unions](#automatically-discriminating-discriminated-unions)
*   [Community and popularity](#community-popularity)

## What is ArkType?

[ArkType](https://github.com/arktypeio/arktype) is a runtime validation library that can infer TypeScript definitions one-to-one and reuse them as highly-optimized validators for your data. 

It is designed to behave as closely as possible to the TypeScript type system. With its rich definition syntax, developers can define types and benefit from the same level of flexibility and expressiveness offered by TypeScript. It helps avoid creating an unsatisfiable type, e.g. by intersecting two objects with incompatible properties. 

Below is an example from [the GitHub](https://github.com/arktypeio/arktype):

```typescript
const data: {
	name: string;
	device: {
		platform: "android" | "ios";
		version?: number;
	};
} | undefined
```

This example demonstrates the concept of the "one-to-one" validator, which means that the definition passed to the "type" function matches the inferred type. This is highly beneficial, as it allows us to define a type once and then use the inferred TypeScript type throughout the codebase, avoiding duplicative declaration. 

Sometimes, the documentation references ArkType as being isomorphic. In the ArkType context, “isomorphic” means that the behavior at compile time and runtime will be identical. In other words, when a developer defines a type in their editor, they can expect to see the same outcome at runtime.

### How ArkType works

Under the hood, the core of the library is a string parser that parses the type definition from a string, then converts the definition to a state object. This state object is then used for evaluation, validation, and type inference. 

The parser has two identical implementations: static and dynamic. To ensure the two implementations generate the same result, the ArkType team developed a special test framework called “attest” (short for ArkType Test). 

The `attest` framework is used to make assertions about compile-time types and runtime behavior simultaneously. This design ensures the “isomorphic” behavior between compile-time and runtime types. 

The following example contains a typo in the definition. The type error shown in the VSCode screenshot reads "Bounded expression ‘boolean’ must be a number, string or array": ![A type error thrown in VS Code because of a typo in the definition](http://blog.logrocket.com/wp-content/uploads/2023/05/typo-error-vs-code.png) Then, during runtime, the exact same error is thrown: ![The same type error thrown at runtime because of a typo in the definition](http://blog.logrocket.com/wp-content/uploads/2023/05/typo-error-runtime.png) In this particular case, the following test asserts that the function with a type error passed to “attest” will throw an error. The test ensures that both the type error and the error generated during runtime should contain an identical, expected message:

```typescript
// dev/test/semantics.test.ts            
it("unboundable", () => {
    // @ts-expect-error
    attest(() => type("unknown<10")).throwsAndHasTypeError(
       writeUnboundableMessage("'unknown'")
    )
})
```

### Benefits of using ArkType <a name="benefits-using-arktype">

Thanks to its unique “isomorphic” parser, ArkType presents some distinct advantages:

*   No dependencies or plugins required in your editor
*   Concise type definitions
*   One-to-one definitions: ArkType definitions mirror TypeScript’s own syntax; usually, your definition will already look just like the type it will be inferred as
*   Clear error messages: Error messages are provided by default, which is especially useful for complex cases like [unions](https://blog.logrocket.com/understanding-discriminated-union-intersection-types-typescript/)
*   Robust type system: In addition to performing validation, ArkType actually contains a full type system under the hood, which means that it can not only check if a given value is allowed, but also check if any arbitrary type is assignable to another. This has lots of potential for advanced scenarios, i.e., automatically determining the best discriminators of discriminated unions
*   Cyclic types and data: ArkType can infer recursive and cyclic types using scopes

## Setting up and using ArkType <a name="setting-up-using-arktype">

To set up ArkType, we need to install it using npm (or another package manager):

```bash
npm install ArkType
```

Once installed, we can import it to our TypeScript code like this:

```typescript
import { type } from "ArkType"
```

Let’s start with an example. Consider the following `pkg` type defined below, which requires an optional `contributors` property that contains two to 10 contributors:

```typescript
export const pkg = type({
   name: "string",
   version: "semver",
   "contributors?": "1<email[]<=10"
})
```

Then, we can use ArkType to validate that a given `externalData` object conforms to the `pkg` type at runtime:

```typescript
// Get validated data or clear, customizable error messages.
const externalData = {
   name: "ArkType",
   version: "1.0.0-alpha",
   contributors: ["david@ArkType.io"]
};
export const { data, problems } = pkg(externalData)

// "contributors must be more than 1 items long (was 1)"
console.log(problems?.summary ?? data)
```

Here, the returned `CheckResult` is destructed into `data` and `problems`. Since the `externalData` object only contains one contributor, a concise error message is shown with the returned `problems.summary` property: ![An error message due to an error with destructuring](http://blog.logrocket.com/wp-content/uploads/2023/05/destructure-error-message.png)

### ArkType type definitions

ArkType type definitions are rich and concise. The system is able to infer TypeScript definitions directly, and supports many of TypeScript’s inbuilt types and operators. The main features include:

*   Primitive and other basic types
*   Support for unions, discriminate unions, and expressions
*   Regex
*   Operators

ArkType support all TypeScript primitive types, including `string`, `number`, and `boolean`. The example below demonstrates the use of a `number` type definition:

```typescript
const numberChecker = type("number");
const numberResult = numberChecker(123); // OK
const notNumberResult = numberChecker("notNumber");
// error: Must be a number (was string)
```

The other basic types, including `array`, `symbol`, and `tuple` are also supported:

```typescript
// Number Array
const numberArrayChecker = type("number[]");
const correctArray = numberArrayChecker([1,2,3]); // OK
const notNumberArray = numberArrayChecker([1,"test"]); // Item at index 1 must be a number (was string)

// Symbol
const symbolChecker = type("symbol");
const symbolResult = symbolChecker(Symbol("key")); // OK
const notSymbolResult = symbolChecker("key"); // Must be a symbol (was string)

// Tuple
const tupleChecker = type(["string", "number"]);
const correctTupleResult = tupleChecker(["hello", 10]); // OK
const wrongTupleResult = tupleChecker(["hello", "world"]); // Item at index 1 must be a number (was string)
```

Union types can be defined as below:

```typescript
type("'a'|'b'|'c'");
```

ArkType also supports simple expressions, such as expressing a number range:

```typescript
const rangeChecker = type({
   "powerLevel?": "1<=number<=100"
})
const withInRange = rangeChecker({powerLevel: 99}); // OK
const outsideRange = rangeChecker({powerLevel: 101});
// powerLevel must be at most 100 (was 101)
```

The expression syntax is flexible, and accommodates string, number, and array data types. It allows the use of the `"<"`, `">"`, `"<="`, `">="`, and `"=="` comparators, and the limit within the expression must be a `number` literal:

```typescript
type("string<=5") // length of string is less or equal to 5
type("number==-3.14159") // the value of number is equal to -3.14159
type("number%2") // the number is divisible with 2
```

Another important ArkType feature is its support of regex, which makes the type definition more powerful. The following example showcases the definition of an object type with a `name` property that must start with an "ark" prefix using regex:

```typescript
type({name: /^ark.*$/});
```

It is worth noting that the regex will be inferred as a `string` by default, but used as regex in the runtime validation. 

Support for TypeScript operators makes using ArkType much easier for TypeScript developers. One commonly used operator is the [`keyof` operator](https://blog.logrocket.com/how-to-use-keyof-operator-typescript/), which extracts the key of properties:

```typescript
const t = type(["keyof", { a: "123", b: "123" }]);
type keyOfT = typeof t.infer; // 'a' | 'b'
```

The `instanceof` operator is another very useful operator:

```typescript
class Ark {}
const arkChecker = type(["instanceof", Ark])
```

The `instanceof` operator functions identically to the JavaScript `instanceof` operator, which verifies whether the prototype property of a constructor is present somewhere in the prototype chain of an object. 

ArkType also provides several custom operators. One of them is `narrow`. The syntax for `narrow` is as below:

```typescript
 ["type", "=>" , condition]
```

It accepts a condition function, in which you can define custom logic to narrow the original type:

```typescript
 const isOdd = (n: number) => n % 2 === 1
 const odd = type(["number", "=>", isOdd])
```

The `narrow` operator can be useful when you want to create a reusable conditional validation rule against a specific type. 

The other available custom operators include:

*   `bound`: allow data to be bounded in the format `"S<N"`, or as a range: `"N<S<N"`, with comparators restricted to `<` or `<=`
*   `|>`: also called the morph operator, used to chain the input type to a function

## Using scopes in ArkType <a name="using-scopes-arktype">

In ArkType, scopes are collections of types that can reference each other. Scopes operate like a module system for ArkType types, and you can create multiple scopes or import one into another. 

Here's an example:

```typescript
const messageTypes = scope({
   info: {
       message: "string"
   },
   error: {
       id: "number",
       code: "string",
   }
}).compile();

const responseScope = scope({
   data: "string",
   exception: "error",
   message: "info"
},
{
   imports: [messageTypes]
}).compile();

responseScope.exception({id: 1, code: 'unhandled exeption'}); // OK
responseScope.exception({id: "1", code: 'unhandled exeption'}); //id must be a number (was string)
```

In the example above, we define a scope called `messageTypes`, which contains two types: `info` and `error`. We then import this scope into the `responseScope`, which references the two type definitions for the `exception` and `message` types. 

Scopes can be useful for reusing common types across multiple type definitions. ArkType also provides a set of inbuil subtypes that can be used within scopes. These subtypes are inferred as `string`s, but they also provide additional validation at runtime. 

For example, below, `email` is an out-of-box subtype that will be validated as a valid email address:

```typescript
scope({
 email: "email",
...})
```

A list of supported subtypes can be found in [the Keywords section](https://arktype.io/docs/api/keywords#validationscope) of the ArkType documentation. 

Within a scope, ArkType has the ability to infer and validate cyclic and recursive types without any additional configuration. This means that if a type is defined in terms of itself, or another type that ultimately refers back to itself, ArkType can handle it. 

The following example illustrates a data structure that contains circular references for the type `package`:

```typescript
const recursiveScope = scope({
    package: {
        name: "string",
        "dependencies?": "package[]",
        "contributors?": "contributor[]"
    },
    contributor: {
        email: "email"
    }
}).compile();

const packageData = {
    name: "ArkType",
    dependencies: [{ name: "typescript" }],
    contributors: [{ email: "david@ArkType.io" }]
};
packageData.dependencies.push(packageData);

recursiveScope.package(packageData); // Ok
packageData.contributors[0].email = "ssalbdivad"; // update with invalid email
recursiveScope.package(packageData); // Error: contributors/0/email must be a valid email (was 'ssalbdivad')
```

## Automatically discriminating discriminated unions

Using its deeply computed internal representation of your types, ArkType automatically discriminates all unions by evaluating the most efficient set of properties to check. In other words, discriminated unions are pre-computed and a set of best “discriminants” are calculated. These pre-computed discriminants will greatly improve performance, especially for large discriminated unions. 

Below is an example:

```typescript
scope({
            rainForest: {
                climate: "'wet'",
                color: "'green'",
                isRainForest: "true"
            },
            desert: { climate: "'dry'", color: "'brown'", isDesert: "true" },
            sky: { climate: "'dry'", color: "'blue'", isSky: "true" },
            ocean: { climate: "'wet'", color: "'blue'", isOcean: "true" }
        })
```

In the above example, the ArkType system automatically discriminates the union, and calculates the best key properties — these are `color` first, then `climate`. 

This means the discrimination process is initiated by checking the `color` property; if it's brown, it knows it's on the `desert` branch. If it's green, it knows it's on the `rainForest` branch. If it's blue, it will discriminate a second time using the `climate` prop. If it's `dry`, it knows it's on the `sky` branch. If it's `wet`, it knows it's on the `ocean` branch. 

All this is done without the user ever having to even know it's happening. But this will significantly speed up validation, especially as unions get larger.

## Community and popularity <a name="community-popularity">

Released in February 2023, ArkType is a relatively new tool that is still in its early stages of development. However, the project is actively maintained and the [author](https://github.com/ssalbdivad) is quick to address any issues that arise. 

It's important to note that as of now, ArkType is still in a 1.0-alpha release, meaning that some APIs may change before a stable 1.0 version is released. Despite this, the project has already gained a fair amount of attention, indicating a growing user base. 

According to the author, there are a few upcoming improvements for ArkType:

*   **Performance improvements**: Currently, ArkType has similar performance to other validation libraries like [Zod](https://blog.logrocket.com/schema-validation-typescript-zod/). The author is implementing a JIT compilation strategy that is expected to make ArkType 50-100 times faster
*   **API documentation**: work on the API documentation is currently in progress

## Summary

In this blog post, we explored the fundamental concepts of using ArkType for runtime validation in TypeScript, covering both basic and advanced use cases. By leveraging ArkType, we can catch errors at runtime that [TypeScript's type checking](https://blog.logrocket.com/methods-for-typescript-runtime-type-checking/) alone may not catch. This can lead to higher code quality and maintainability. 

Although ArkType is still in its early stages, it has the potential to become a widely used validation tool. If you're starting a new project, it's worth considering trying out. However, bear in mind that some minor API changes may occur as ArkType approaches version 1.0. 

To learn more about ArkType and its features, take a look at the [official documentation](https://arktype.io/).

---

##[LogRocket](https://lp.logrocket.com/blg/typescript-signup): Full visibility into your web and mobile apps

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/typescript-signup)

[LogRocket](https://lp.logrocket.com/blg/typescript-signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking usexrs for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free](https://lp.logrocket.com/blg/typescript-signup).