
Arktype Validation
- 101 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
arktype-validation is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- arktype-validation
- AI & Agent Building
- AI-coding skill
Arktype Validation by the numbers
- 101 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,322 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill arktype-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 101 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
ArkType Validation
Overview
ArkType is a TypeScript-native runtime validation library that defines schemas using string expressions mirroring TypeScript syntax, providing editor autocomplete, syntax highlighting, and optimized validators. Use when building type-safe APIs, validating JSON payloads, or replacing Zod with a more TypeScript-idiomatic approach. Not suitable for projects that need Zod ecosystem compatibility or JSON Schema output.
Package: arktype
Quick Reference
| Pattern | Usage |
|---|---|
type({ key: "string" }) | Define object schema |
type("string") | Primitive type |
"string.email", "string.url" | Built-in string validators |
"string.trim", "string.lower" | Built-in string morphs (transforms) |
"string.json.parse" | Parse JSON string to validated object |
"number > 0", "string >= 1" | Inline constraints |
| `"string \ | number"` |
| `"'a' \ | 'b' \ |
"string[]" | Array types |
"key?": "string" | Optional properties |
"key = 'default'" | Default values |
.pipe(), .to() | Transform output (morphs) |
.narrow() | Custom validation (like Zod refine) |
.pick(), .omit() | Object property selection |
.merge() | Combine object types |
scope({...}).export() | Named type scopes with cross-references |
type("<t>", { box: "t" }) | Generic type definitions |
type.errors | Error handling (check instanceof type.errors) |
.assert(data) | Throws on invalid input instead of returning |
"(number % 2)#even" | Branding — type-only validated marker |
"0 <= number <= 100" | Compact range constraints |
configure() | Global defaults (from arktype/config) |
match() | Type-safe pattern matching (2.1) |
"+" : "reject" | Inline undeclared key handling |
arkenv({ PORT: "number" }) | Typesafe env var validation (ArkEnv) |
arkenvVitePlugin(Env) | Build-time env validation for Vite |
Common Mistakes
| Mistake | Fix |
|---|---|
type("string.email()") with parens | type("string.email") (no parens) |
Checking errors with === null | Use instanceof type.errors |
{ key: "string?" } for optional | { "key?": "string" } (question mark on key) |
Importing from arktype/types | Import type and scope from "arktype" |
Nested type() in string expressions | Use scope() for cross-referencing types |
Raw .pipe() without error handling | Use .pipe.try() for operations that can throw |
"string.lowercase" for case morph | "string.lower" (also "string.upper") |
Configuring after importing arktype | Import arktype/config before arktype |
Manual process.env parsing | Use arkenv() for auto-coercion and validation |
Delegation
Use this skill for ArkType schema definitions, runtime validation, morphs/transforms, scopes, and type inference. For Zod-based validation, delegate to the zod-validation skill.
References
- Schema Types — primitives, string keywords, number constraints, objects, arrays, tuples, unions, optional, defaults
- Morphs and Scopes — pipe, morph transforms, narrow validation, scopes, recursive types, generics, global configuration, pattern matching
- Common Patterns — JSON parsing, form validation, API responses, error handling, ArkEnv environment variables, Vite plugin, comparison with Zod
Common Patterns
JSON Parsing and Validation
import { type } from 'arktype';
const parseConfig = type('string.json.parse').to({
host: 'string',
port: 'number.integer > 0 & number < 65536',
'ssl = false': 'boolean',
});
const result = parseConfig('{"host": "localhost", "port": 3000}');
if (result instanceof type.errors) {
console.error(result.summary);
} else {
console.log(result.host); // "localhost"
}Error Handling
ArkType returns errors as type.errors instances instead of throwing:
const User = type({
name: 'string >= 1',
email: 'string.email',
age: 'number.integer >= 0',
});
const result = User({ name: '', email: 'bad', age: -1 });
if (result instanceof type.errors) {
// Full error summary
console.error(result.summary);
// "name must be at least length 1 (was 0)
// email must be an email address (was "bad")
// age must be at least 0 (was -1)"
// Iterate individual errors
for (const error of result) {
console.log(error.path, error.message);
}
}Form Validation
const LoginSchema = type({
email: 'string.email',
password: 'string >= 8',
'rememberMe = false': 'boolean',
});
const RegisterSchema = type({
name: 'string >= 1 & string <= 100',
email: 'string.email',
password: 'string >= 8 & string <= 100',
confirm: 'string',
}).narrow((data, ctx) => {
if (data.password !== data.confirm) {
return ctx.reject({
expected: 'passwords to match',
path: ['confirm'],
});
}
return true;
});API Response Pattern
const ApiResponse = type('<t>', {
data: 't',
error: 'string | null',
status: "'success' | 'error'",
});
const UserResponse = ApiResponse({
id: 'string.uuid',
name: 'string',
email: 'string.email',
});
type UserResponse = typeof UserResponse.infer;Environment Variables with ArkEnv
ArkEnv wraps ArkType for environment variable validation with automatic coercion, defaults, and framework plugins.
Install alongside ArkType:
pnpm add arkenv arktypeBasic Usage
import arkenv from 'arkenv';
const env = arkenv({
HOST: "string.ip | 'localhost'",
PORT: '0 <= number.integer <= 65535',
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
DEBUGGING: 'boolean = false',
SESSION_SECRET: 'string >= 32',
'API_KEY?': 'string',
});
env.PORT; // number (auto-coerced from string)
env.DEBUGGING; // boolean
env.NODE_ENV; // 'development' | 'production' | 'test'ArkEnv auto-coerces environment strings to the target type. If validation fails, the app exits with a clear error:
ArkEnvError: Errors found while validating environment variables
HOST must be a string or "localhost" (was missing)
PORT must be a number (was a string)Arrays
Comma-separated values are parsed automatically:
import arkenv from 'arkenv';
import { type } from 'arkenv/arktype';
const env = arkenv({
ALLOWED_ORIGINS: type('string[]').default(() => ['localhost']),
FEATURE_FLAGS: type('string[]').default(() => []),
});ALLOWED_ORIGINS=http://localhost:3000,https://example.comLazy Validation with Proxy
Defer validation until the first property access. This allows .env files or test setup to load before validation runs:
import arkenv from 'arkenv';
import { type } from 'arkenv/arktype';
export const Env = type({
DATABASE_URL: 'string > 0',
BETTER_AUTH_SECRET: 'string >= 32',
BETTER_AUTH_URL: 'string.url',
PASSWORD_MIN_LENGTH: '6 <= number.integer <= 128 = 8',
TRUSTED_ORIGINS: 'string > 0',
'OTEL_EXPORTER_OTLP_ENDPOINT?': 'string.url',
VITE_APP_TITLE: "string > 0 = 'My App'",
});
type ValidatedEnv = typeof Env.infer;
let _env: ValidatedEnv | undefined;
function getEnv(): ValidatedEnv {
if (!_env) {
if (process.env.SKIP_ENV_VALIDATION === 'true') {
return process.env as unknown as ValidatedEnv;
}
_env = arkenv(Env, {
env: process.env,
coerce: true,
onUndeclaredKey: 'delete',
});
}
return _env;
}
export const env = new Proxy({} as ValidatedEnv, {
get(_, prop: string) {
return getEnv()[prop as keyof ValidatedEnv];
},
});SKIP_ENV_VALIDATION bypasses validation for CI builds or test environments that don't set all variables.
Vite Plugin
The @arkenv/vite-plugin validates env vars at build time and auto-filters VITE_* prefixed variables for the client bundle:
pnpm add @arkenv/vite-pluginDefine the schema in a shared config file so both vite.config and type augmentation can import it:
// src/configs/env.ts
import { type } from 'arkenv/arktype';
export const Env = type({
PORT: 'number.port',
VITE_API_URL: 'string',
VITE_FEATURE_FLAGS: 'boolean = false',
});Use @dotenvx/dotenvx to load .env files before the plugin validates. The flow convention loads files in dotenv-flow order (.env.local, .env):
// vite.config.ts
import arkenvVitePlugin from '@arkenv/vite-plugin';
import { config } from '@dotenvx/dotenvx';
import tailwindcss from '@tailwindcss/vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { Env } from './src/configs/env';
config({ convention: 'flow', quiet: true });
export default defineConfig({
plugins: [
viteTsConfigPaths({ projects: ['./tsconfig.json'] }),
arkenvVitePlugin(Env),
tailwindcss(),
tanstackStart(),
viteReact(),
],
server: { port: 3000 },
});The plugin auto-filters to only expose VITE_* prefixed variables to the client bundle — server-only variables like DATABASE_URL are excluded.
Type-safe import.meta.env in client code via vite-env.d.ts:
/// <reference types="vite/client" />
import type { ImportMetaEnvAugmented as ArkenvImportMetaEnvAugmented } from '@arkenv/vite-plugin';
import type { Env } from '@/configs/env';
type ImportMetaEnvAugmented = ArkenvImportMetaEnvAugmented<typeof Env>;
interface ViteTypeOptions {
strictImportMetaEnv: unknown;
}
interface ImportMetaEnv extends ImportMetaEnvAugmented {}const apiUrl = import.meta.env.VITE_API_URL; // string — validated at build
// import.meta.env.PORT — TypeScript error, server-onlyStandard Schema Validators
ArkEnv also works with Zod, Valibot, or any Standard Schema validator:
import arkenv from 'arkenv';
import { z } from 'zod';
const env = arkenv({
PORT: z.coerce.number().int().min(0).max(65535),
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
});Discriminated Unions
const Event = type(
{
type: "'click'",
x: 'number',
y: 'number',
},
'|',
{
type: "'keypress'",
key: 'string',
'modifiers?': 'string[]',
},
'|',
{
type: "'scroll'",
deltaY: 'number',
},
);Zod vs ArkType Comparison
| Concept | Zod | ArkType |
|---|---|---|
| Define object | z.object({ name: z.string() }) | type({ name: "string" }) |
| Optional | z.string().optional() | "key?": "string" |
| Default | z.string().default("x") | "key = 'x'": "string" |
| Union | z.union([z.string(), z.number()]) | `type("string \ |
| Array | z.array(z.string()) | type("string[]") |
z.email() | type("string.email") | |
| Min length | z.string().min(1) | type("string >= 1") |
| Integer | z.number().int() | type("number.integer") |
| Transform | .transform(fn) | .pipe(fn) or ["string", "=>", fn] |
| Custom validate | .refine(fn) | .narrow(fn) |
| Parse | schema.parse(data) | schema(data) |
| Safe parse | schema.safeParse(data) | schema(data) (returns errors, not throws) |
| Infer type | z.infer<typeof Schema> | typeof Schema.infer |
| Recursive | z.lazy(() => schema) | scope({...}).export() |
| Scoped types | N/A | scope({...}).export() |
Morphs and Scopes
Morphs (Transforms)
Transform validated input using .pipe() or the => tuple syntax:
import { type } from 'arktype';
// Pipe syntax — chain transformations
const trimmed = type('string').pipe((s) => s.trim());
// Tuple syntax
const trimmed2 = type(['string', '=>', (s) => s.trim()]);
// Chain multiple steps
const trimToNonEmpty = type.pipe(
type.string,
(s) => s.trimStart(),
type.string.atLeastLength(1),
);Pipe with Validation (.to)
.to() is shorthand for .pipe() with a single output validator:
const parseJson = type('string.json.parse').to({
name: 'string',
version: 'string.semver',
});
const out = parseJson('{ "name": "arktype", "version": "2.0.0" }');
if (!(out instanceof type.errors)) {
console.log(`${out.name}:${out.version}`);
}Safe Pipe (.pipe.try)
Use .pipe.try() for operations that can throw:
const parseJson = type('string').pipe.try(
(s): object => JSON.parse(s),
type({
name: 'string',
version: 'string.semver',
}),
);Narrow (Custom Validation)
Add custom validation logic without transforming the output:
const even = type('number.integer').narrow((n, ctx) => {
if (n % 2 !== 0) {
return ctx.reject({ expected: 'an even number' });
}
return true;
});
const passwordMatch = type({
password: 'string >= 8',
confirm: 'string',
}).narrow((data, ctx) => {
if (data.password !== data.confirm) {
return ctx.reject({
expected: 'passwords to match',
path: ['confirm'],
});
}
return true;
});Assert (Throwing Validation)
.assert() throws on invalid input instead of returning type.errors:
import { type } from 'arktype';
const User = type({
name: 'string >= 1',
email: 'string.email',
});
const user = User.assert({ name: 'Alice', email: 'alice@example.com' });
// Returns validated data or throws AggregateErrorUse assert when invalid data is a programmer error rather than expected user input.
Branding
Add type-only symbols so only directly validated values satisfy the type:
import { type } from 'arktype';
const Even = type('(number % 2)#even');
type Even = typeof Even.infer;
const good: Even = Even.assert(2);
// const bad: Even = 5; // TypeScript error — not brandedFluent API:
const PositiveInt = type.number.moreThan(0).brand('positiveInt');
type PositiveInt = typeof PositiveInt.infer;Scopes
Define named types that can reference each other. Use scope() for complex type systems, type.module() for quick groups:
import { scope } from 'arktype';
const types = scope({
User: {
name: 'string',
email: 'string.email',
'posts?': 'Post[]',
},
Post: {
title: 'string',
content: 'string',
author: 'User',
},
}).export();
// Access types
const user = types.User({ name: 'Alice', email: 'alice@example.com' });
type User = typeof types.User.infer;Quick Modules
type.module() is a lighter alternative when you don't need scope's full power:
import { type } from 'arktype';
const auth = type.module({
Credentials: {
username: 'string >= 3',
password: 'string >= 8',
},
Token: {
value: 'string',
expiresAt: 'Date',
},
});
const creds = auth.Credentials({ username: 'alice', password: 'secret123' });Recursive Types
Scopes enable recursive/cyclic type definitions:
const types = scope({
Category: {
name: 'string',
'children?': 'Category[]',
},
}).export();
const tree = types.Category({
name: 'root',
children: [
{ name: 'child1' },
{ name: 'child2', children: [{ name: 'grandchild' }] },
],
});Self-referencing within a scope:
const types = scope({
Package: {
name: 'string',
'dependencies?': 'Package[]',
'contributors?': 'Contributor[]',
},
Contributor: {
email: 'string.email',
'packages?': 'Package[]',
},
}).export();Generics
Define reusable generic type constructors using string-based angle bracket syntax:
const boxOf = type('<t>', { box: 't' });
const stringBox = boxOf('string');
// { box: string }
// Constrained generics
const nonEmpty = type('<arr extends unknown[]>', 'arr > 0');
// Multi-parameter generics
const either = type('<a, b>', 'a | b');
const stringOrNumber = either('string', 'number');Scoped Generics
const types = scope({
'box<t, u>': {
box: 't | u',
},
bitBox: 'box<0, 1>',
}).export();
const out = types.bitBox({ box: 0 });Global Configuration
Import from arktype/config before importing from arktype:
import { configure } from 'arktype/config';
configure({ onUndeclaredKey: 'delete' });
// Now import arktype
import { type } from 'arktype';Configuration applied after arktype is imported will not affect built-in keywords that were already parsed.
Pattern Matching (2.1)
The match function provides type-safe pattern matching:
import { type, match } from 'arktype';
const describe = match({
string: (s) => `a string: ${s}`,
number: (n) => `a number: ${n}`,
default: 'something else',
});
describe('hello'); // "a string: hello"
describe(42); // "a number: 42"Standard Schema
ArkType co-authors the Standard Schema spec with Zod and Valibot. Any ArkType schema works as a Standard Schema validator — libraries like TanStack Form, ArkEnv, and tRPC can consume it without coupling to a specific validation library.
arkregex
The arkregex package infers string literal types from regular expressions at compile time with zero runtime overhead:
import { regex } from 'arkregex';
const semver = regex('^(\\d*)\\.(\\d*)\\.(\\d*)$');
// Regex<`${bigint}.${bigint}.${bigint}`, { captures: [...] }>
const email = regex('^(?<name>\\w+)@(?<domain>\\w+\\.\\w+)$');
// Named capture groups are typedFor most validation, ArkType's built-in string.email, string.semver, or inline /regex/ syntax is sufficient. Use arkregex when you need typed capture groups or compile-time string type inference.
Schema Types
Primitives
ArkType supports both string syntax and fluent API:
import { type } from 'arktype';
// String syntax
const str = type('string');
const num = type('number');
const bool = type('boolean');
const bigint = type('bigint');
const sym = type('symbol');
const date = type('Date');
// Fluent API
const str2 = type.string;
const num2 = type.number;
const bool2 = type.boolean;String Keywords
Built-in string format validators:
// Validators (return string)
type('string.email');
type('string.url');
type('string.uuid');
type('string.uuid.v4');
type('string.semver');
type('string.ip');
type('string.ip.v4');
type('string.ip.v6');
type('string.date');
type('string.date.iso');
type('string.numeric');
type('string.integer');
type('string.alpha');
type('string.alphanumeric');
type('string.digits');
type('string.hex');
type('string.creditCard');
type('string.base64');
type('string.base64.url');
type('string.json');
type('string.host'); // Hostname (IP or domain)
// Morphs (transform the value)
type('string.trim'); // Trims whitespace
type('string.lower'); // Transforms to lowercase
type('string.upper'); // Transforms to uppercase
type('string.capitalize'); // Capitalizes first letter
type('string.normalize.NFC'); // Unicode normalization
type('string.json.parse'); // Parses JSON string at runtime
type('string.numeric.parse'); // Parses numeric string to number
type('string.integer.parse'); // Parses integer string to number
type('string.date.parse'); // Parses date string to Date
type('string.date.iso.parse'); // Parses ISO date string to Date
type('string.url.parse'); // Parses URL string to URL instanceString Constraints
type('string >= 1'); // Min length 1
type('string <= 100'); // Max length 100
type('string >= 1 & string <= 100'); // Range (intersection)
type('5 <= string <= 20'); // Range (compact syntax)
type('string == 5'); // Exact length
// Regex patterns
type('/^[a-z]+$/');
type({ key: /^[a-z]+$/ }); // RegExp objectNumber Constraints
type('number > 0'); // Positive
type('number >= 0'); // Non-negative
type('number < 100'); // Less than 100
type('number % 2'); // Divisible by 2 (even)
type('number.integer'); // Integer only
type('number.safe'); // Safe integer range
type('number.epoch'); // Unix timestamp (safe integer >= 0)
type('number.port'); // Valid port (integer 0-65535)
type('number > 0 & number < 100'); // Range (intersection)
type('0 <= number <= 100'); // Range (compact syntax)
type('1 <= number.integer <= 5'); // Constrained range
type('0 <= number.integer % 5 <= 100'); // Range + divisibility
// Fluent API constraints
type.number.atLeast(0);
type.number.atMost(100);
type.number.moreThan(0);
type.number.lessThan(100);
type.number.divisibleBy(5);Objects
const User = type({
name: 'string',
email: 'string.email',
age: 'number >= 0',
});
// Infer TypeScript type
type User = typeof User.infer;
// { name: string; email: string; age: number }Optional Properties
const User = type({
name: 'string',
'bio?': 'string', // Optional (question mark on key)
'age?': 'number',
});Default Values
const Config = type({
'theme = "light"': 'string',
'retries = 3': 'number',
'debug = false': 'boolean',
});Undeclared Key Handling
// Inline syntax with "+"
const Strict = type({
'+': 'reject',
name: 'string',
});
// Fluent API
const StrictFluent = type({
name: 'string',
}).onUndeclaredKey('reject'); // Reject extra keys
const Strip = type({
name: 'string',
}).onUndeclaredKey('delete'); // Strip extra keys
// Deep undeclared key handling (nested objects)
const DeepStrip = type({
name: 'string',
nested: {
preserved: 'string',
},
}).onDeepUndeclaredKey('delete');Object Modifiers
const User = type({
name: 'string',
email: 'string.email',
'age?': 'number',
});
User.pick('name', 'email'); // Only name and email
User.omit('age'); // Remove age
User.merge({
role: "'admin' | 'user'",
});Arrays and Tuples
// Arrays
type('string[]');
type('number[]');
type('string.email[]');
// In objects
const Team = type({
members: 'string[]',
scores: 'number[]',
});
// Tuples
type(['string', 'number']); // [string, number]
type(['string', 'number', '...', 'boolean[]']); // [string, number, ...boolean[]]Unions and Literals
// Unions
type('string | number');
type("'success' | 'error' | 'pending'");
// Literal values
type('true');
type('false');
type('0');
type("'active'");
// In objects
const Event = type({
type: "'click' | 'hover' | 'focus'",
target: 'string',
'data?': 'unknown',
});Intersections
// Combine constraints
type('number > 0 & number < 100 & number.integer');
type('string >= 1 & string <= 50');
// Object intersections
const Named = type({ name: 'string' });
const Aged = type({ age: 'number' });
const Person = Named.and(Aged);Date Constraints
// String syntax with date literals
const Bounded = type({
dateInThePast: `Date < ${Date.now()}`,
dateAfter2000: "Date > d'2000-01-01'",
dateAtOrAfter1970: 'Date >= 0',
});
// Fluent API
const FluentBounded = type({
dateInThePast: type.Date.earlierThan(Date.now()),
dateAfter2000: type.Date.laterThan('2000-01-01'),
dateAtOrAfter1970: type.Date.atOrAfter(0),
});Index Signatures and keyof
// Index signatures
type('Record<string, unknown>');
const Dict = type({ '[string]': 'number' });
// Extract object keys as union type
const User = type({
name: 'string',
email: 'string.email',
});
const UserKey = User.keyof();
type UserKey = typeof UserKey.infer; // "name" | "email"Special Types
type('unknown'); // Any value
type('never'); // No value
type('void'); // undefined
type('null');
type('undefined');
type('object'); // Non-null object
type('Record<string, unknown>'); // String-keyed object