diff --git a/apps/playgrounds/node/index.ts b/apps/playgrounds/node/index.ts
index 8a3adf28f..ebc06e543 100644
--- a/apps/playgrounds/node/index.ts
+++ b/apps/playgrounds/node/index.ts
@@ -1,6 +1,6 @@
import arkenv, { type } from "arkenv";
-const env = arkenv({
+const envSchema = type({
HOST: "string.host",
PORT: "number.port",
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
@@ -8,6 +8,8 @@ const env = arkenv({
DEBUG: "boolean = true",
});
+const env = arkenv(envSchema, process.env);
+
// Automatically validate and parse process.env
// TypeScript knows the ✨exact✨ types!
const host = env.HOST;
diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx
index fb234ced4..095755df3 100644
--- a/apps/www/app/layout.tsx
+++ b/apps/www/app/layout.tsx
@@ -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";
@@ -48,7 +48,7 @@ export default function Layout({ children }: { children: ReactNode }) {
enableSystem: true,
}}
>
-
+ {/*
🎉 We are now featured on
!
-
+ */}
{children}
diff --git a/apps/www/content/docs/how-to/reuse-schemas.mdx b/apps/www/content/docs/how-to/reuse-schemas.mdx
new file mode 100644
index 000000000..a3f526906
--- /dev/null
+++ b/apps/www/content/docs/how-to/reuse-schemas.mdx
@@ -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:
+
+
+
+ ```ts twoslash
+ import { type } from 'arkenv';
+
+ export const envSchema = type({
+ DATABASE_HOST: "string.host",
+ DATABASE_PORT: "number.port",
+ API_KEY: "string",
+ });
+ ```
+
+
+ ```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);
+ ```
+
+
+ ```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);
+ ```
+
+
+
+## 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.
+
diff --git a/apps/www/content/docs/meta.json b/apps/www/content/docs/meta.json
index e23927835..6a5416027 100644
--- a/apps/www/content/docs/meta.json
+++ b/apps/www/content/docs/meta.json
@@ -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)"
]
}
diff --git a/apps/www/content/docs/morphs.mdx b/apps/www/content/docs/morphs.mdx
index d067ed425..592c5e721 100644
--- a/apps/www/content/docs/morphs.mdx
+++ b/apps/www/content/docs/morphs.mdx
@@ -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.
diff --git a/packages/arkenv/src/create-env.ts b/packages/arkenv/src/create-env.ts
index d2d0487a9..bb7b962e7 100644
--- a/packages/arkenv/src/create-env.ts
+++ b/packages/arkenv/src/create-env.ts
@@ -6,6 +6,20 @@ type RuntimeEnvironment = Record;
export type EnvSchema = type.validate;
+/**
+ * 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 extends (
+ value: Record,
+) => infer R
+ ? R extends type.errors
+ ? never
+ : R
+ : T extends type.Any
+ ? U
+ : never;
+
/**
* TODO: If possible, find a better type than "const T extends Record",
* and be as close as possible to the type accepted by ArkType's `type`.
@@ -13,16 +27,33 @@ export type EnvSchema = type.validate;
/**
* 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>(
def: EnvSchema,
+ env?: RuntimeEnvironment,
+): distill.Out>;
+export function createEnv(
+ def: T,
+ env?: RuntimeEnvironment,
+): InferType;
+export function createEnv>(
+ def: EnvSchema | type.Any,
+ env?: RuntimeEnvironment,
+): distill.Out> | InferType;
+export function createEnv>(
+ def: EnvSchema | type.Any,
env: RuntimeEnvironment = process.env,
-): distill.Out> {
- const schema = $.type.raw(def);
+): distill.Out> | InferType {
+ // 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);
const validatedEnv = schema(env);
diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts
index e109dc645..ce7015c24 100644
--- a/packages/vite-plugin/src/index.ts
+++ b/packages/vite-plugin/src/index.ts
@@ -1,5 +1,6 @@
import type { EnvSchema } from "arkenv";
import { createEnv } from "arkenv";
+import type { type } from "arktype";
import { loadEnv, type Plugin } from "vite";
/**
@@ -9,10 +10,16 @@ import { loadEnv, type Plugin } from "vite";
export default function arkenv>(
options: EnvSchema,
+): Plugin;
+export default function arkenv(options: type.Any): Plugin;
+export default function arkenv>(
+ options: EnvSchema | 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
diff --git a/tooling/playwright-www/tests/docs-navigation.test.ts b/tooling/playwright-www/tests/docs-navigation.test.ts
index f1ab66979..0140b489b 100644
--- a/tooling/playwright-www/tests/docs-navigation.test.ts
+++ b/tooling/playwright-www/tests/docs-navigation.test.ts
@@ -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
@@ -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");
}
});