Skip to content
4 changes: 3 additions & 1 deletion apps/playgrounds/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import arkenv, { type } from "arkenv";

const env = arkenv({
const envSchema = type({
HOST: "string.host",
PORT: "number.port",
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
ALLOWED_ORIGINS: type("string[]").default(() => []),
DEBUG: "boolean = true",
});

const env = arkenv(envSchema, process.env);

// Automatically validate and parse process.env
// TypeScript knows the ✨exact✨ types!
const host = env.HOST;
Expand Down
6 changes: 3 additions & 3 deletions apps/www/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Banner } from "fumadocs-ui/components/banner";
// import { Banner } from "fumadocs-ui/components/banner";
import "./globals.css";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
Expand Down Expand Up @@ -48,7 +48,7 @@ export default function Layout({ children }: { children: ReactNode }) {
enableSystem: true,
}}
>
<Banner variant="rainbow" id="arktype-feature-banner">
{/* <Banner variant="rainbow" id="arktype-feature-banner">
🎉 We are now featured on&nbsp;
<a
href="https://arktype.io/docs/ecosystem#arkenv"
Expand All @@ -59,7 +59,7 @@ export default function Layout({ children }: { children: ReactNode }) {
arktype.io
</a>
!
</Banner>
</Banner> */}
{children}
<SpeedInsights />
<Analytics />
Expand Down
163 changes: 163 additions & 0 deletions apps/www/content/docs/how-to/reuse-schemas.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
title: How to reuse your schema
description: Define your schema once and reuse it across your application.
icon: New
---

ArkEnv supports both raw schema objects and type definitions created with ArkType's `type()` function. Use type definitions when you need to validate the same environment variables in multiple places.

## Basic usage

Recall that with ArkEnv, defining your schema is done at the same time as you parse the environment variables:

```ts twoslash
import arkenv from 'arkenv';

const env = arkenv({
HOST: "string.host",
PORT: "number.port",
DEBUG: "boolean",
});
```

But what if you wanted to share this schema across your app and validate+parse it against different environments?

This might sound like a niche use-case, but it comes up more often than you'd think - especially with tools like [Vite](https://github.com/yamcodes/arkenv/tree/main/apps/playgrounds/vite), [Next.js](https://nextjs.org/), or [Bun](https://github.com/yamcodes/arkenv/tree/main/examples/with-bun), where multiple runtimes might need access to the same environment variables. Even in something as simple as a Vite+React setup, you might want your `.env` schema available both [inside your `vite.config.ts` (Node.js)](https://vite.dev/config/#using-environment-variables-in-config) and [in client code (Vite dev server)](https://vite.dev/guide/env-and-mode). See the [Node.js example](https://github.com/yamcodes/arkenv/tree/main/examples/basic) for a basic setup, or the [Vite playground](https://github.com/yamcodes/arkenv/tree/main/apps/playgrounds/vite) for a complete multi-runtime configuration.

That's where type definitions start making sense: define your schema once with `type()`, and reuse it wherever you need.

```ts twoslash
import arkenv, { type } from 'arkenv';

const envSchema = type({
HOST: "string.host",
PORT: "number.port",
DEBUG: "boolean",
});

// Use it in multiple places with full type inference
const env1 = arkenv(envSchema, process.env);
const env2 = arkenv(envSchema, { HOST: "localhost", PORT: "3000", DEBUG: "true" });

// TypeScript knows the exact types
const host: string = env1.HOST;
const port: number = env1.PORT;
```

## Sharing across modules

Export a type definition from one module and import it in others:

<Tabs items={['config/env-schema.ts', 'config/database.ts', 'config/api.ts']}>
<Tab value="config/env-schema.ts">
```ts twoslash
import { type } from 'arkenv';

export const envSchema = type({
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",
API_KEY: "string",
});
```
</Tab>
<Tab value="config/database.ts">
```ts twoslash
// @filename: env-schema.ts
import { type } from 'arkenv';

export const envSchema = type({
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",
API_KEY: "string",
});
// @filename: database.ts
// ---cut---
import arkenv from 'arkenv';
import { envSchema } from './env-schema';

export const dbEnv = arkenv(envSchema, process.env);
```
</Tab>
<Tab value="config/api.ts">
```ts twoslash
// @filename: env-schema.ts
import { type } from 'arkenv';

export const envSchema = type({
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",
API_KEY: "string",
});
// @filename: api.ts
// ---cut---
import arkenv from 'arkenv';
import { envSchema } from './env-schema';

export const apiEnv = arkenv(envSchema, process.env);
```
</Tab>
</Tabs>

## Type inference

Type definitions provide the same type safety as raw schema objects:

```ts twoslash
import arkenv, { type } from 'arkenv';

const envSchema = type({
PORT: "number.port",
HOST: "string.host",
TIMEOUT: "number >= 0",
});

const env = arkenv(envSchema, process.env);

const port: number = env.PORT;
const host: string = env.HOST;
const timeout: number = env.TIMEOUT;
```

## Different environment sources

Use the same schema with different environment sources:

```ts twoslash
import arkenv, { type } from 'arkenv';

const envSchema = type({
API_URL: "string",
API_KEY: "string",
});

const productionEnv = arkenv(envSchema, process.env);
const testEnv = arkenv(envSchema, {
API_URL: "http://localhost:3000",
API_KEY: "test-key",
});
```

## Mixing approaches

You can mix raw schema objects and type definitions in the same codebase:

```ts twoslash
import arkenv, { type } from 'arkenv';

// Raw schema object (simple, one-off)
const simpleEnv = arkenv({
NODE_ENV: "'development' | 'production'",
});

// Type definition (reusable)
const complexSchema = type({
DATABASE_HOST: "string.host",
DATABASE_PORT: "number.port",
DEBUG: "boolean",
});

const complexEnv = arkenv(complexSchema, process.env);
```

Both approaches provide the same validation and type safety. Use raw objects for simple, one-off schemas, and type definitions when you need to reuse the schema.

3 changes: 2 additions & 1 deletion apps/www/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"[VS Code & Cursor](/docs/integrations/vscode)",
"[JetBrains IDEs](/docs/integrations/jetbrains)",
"---How-to---",
"[Load environment variables](/docs/how-to/load-environment-variables)"
"[Load environment variables](/docs/how-to/load-environment-variables)",
"[New][Reuse your schema](/docs/how-to/reuse-schemas)"
]
}
1 change: 0 additions & 1 deletion apps/www/content/docs/morphs.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
title: Morphs
icon: New
---

Since environment variables are defined as strings, ArkEnv automatically transforms certain ArkType keywords when imported through ArkEnv instead of directly from ArkType. These "morphs" provide environment-variable-specific behavior that's optimized for configuration management.
Expand Down
37 changes: 34 additions & 3 deletions packages/arkenv/src/create-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,54 @@ type RuntimeEnvironment = Record<string, string | undefined>;

export type EnvSchema<def> = type.validate<def, (typeof $)["t"]>;

/**
* Extract the inferred type from an ArkType type definition by checking its call signature
* When a type definition is called, it returns either the validated value or type.errors
*/
type InferType<T> = T extends (
value: Record<string, string | undefined>,
) => infer R
? R extends type.errors
? never
: R
: T extends type.Any<infer U, infer _Scope>
? U
: never;

/**
* TODO: If possible, find a better type than "const T extends Record<string, unknown>",
* and be as close as possible to the type accepted by ArkType's `type`.
*/

/**
* Create an environment variables object from a schema and an environment
* @param def - The environment variable schema
* @param def - The environment variable schema (raw object or type definition created with `type()`)
* @param env - The environment variables to validate, defaults to `process.env`
* @returns The validated environment variable schema
* @throws An {@link ArkEnvError | error} if the environment variables are invalid.
*/
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T>,
env?: RuntimeEnvironment,
): distill.Out<type.infer<T, (typeof $)["t"]>>;
export function createEnv<T extends type.Any>(
def: T,
env?: RuntimeEnvironment,
): InferType<T>;
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T> | type.Any,
env?: RuntimeEnvironment,
): distill.Out<type.infer<T, (typeof $)["t"]>> | InferType<typeof def>;
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T> | type.Any,
env: RuntimeEnvironment = process.env,
): distill.Out<type.infer<T, (typeof $)["t"]>> {
const schema = $.type.raw(def);
): distill.Out<type.infer<T, (typeof $)["t"]>> | InferType<typeof def> {
// If def is a type definition (has assert method), use it directly
// Otherwise, use raw() to convert the schema definition
const schema =
typeof def === "function" && "assert" in def
? def
: $.type.raw(def as EnvSchema<T>);

const validatedEnv = schema(env);

Expand Down
7 changes: 7 additions & 0 deletions packages/vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
import type { type } from "arktype";
import { loadEnv, type Plugin } from "vite";

/**
Expand All @@ -9,10 +10,16 @@ import { loadEnv, type Plugin } from "vite";

export default function arkenv<const T extends Record<string, unknown>>(
options: EnvSchema<T>,
): Plugin;
export default function arkenv(options: type.Any): Plugin;
export default function arkenv<const T extends Record<string, unknown>>(
options: EnvSchema<T> | type.Any,
): Plugin {
return {
name: "@arkenv/vite-plugin",
config(_config, { mode }) {
// createEnv accepts both EnvSchema and type.Any at runtime
// We use overloads above to provide external type precision
const env = createEnv(options, loadEnv(mode, process.cwd(), ""));

// Expose transformed environment variables through Vite's define option
Expand Down
14 changes: 6 additions & 8 deletions tooling/playwright-www/tests/docs-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,9 @@ test.describe("Documentation Navigation", () => {
if (githubLinkCount > 0) {
const firstGithubLink = githubLinks.first();
await expect(firstGithubLink).toHaveAttribute("target", "_blank");
await expect(firstGithubLink).toHaveAttribute(
"rel",
"noreferrer noopener",
);
const rel = await firstGithubLink.getAttribute("rel");
expect(rel).toContain("noopener");
expect(rel).toContain("noreferrer");
}

// Check for ArkType links
Expand All @@ -159,10 +158,9 @@ test.describe("Documentation Navigation", () => {
if (arkTypeLinkCount > 0) {
const firstArkTypeLink = arkTypeLinks.first();
await expect(firstArkTypeLink).toHaveAttribute("target", "_blank");
await expect(firstArkTypeLink).toHaveAttribute(
"rel",
"noopener noreferrer",
);
const rel = await firstArkTypeLink.getAttribute("rel");
expect(rel).toContain("noopener");
expect(rel).toContain("noreferrer");
}
});

Expand Down
Loading