Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/array-defaults-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"arkenv": minor
---

#### Support array defaults using type().default() syntax

Fix to an issue where `type("array[]").default(() => [...])` syntax was not accepted by `createEnv` due to overly restrictive type constraints. The function now accepts any string-keyed record while still maintaining type safety through ArkType's validation system.

### New Features
- Array defaults to empty using `type("string[]").default(() => [])` syntax
- Support for complex array types with defaults
- Mixed schemas combining string-based and type-based defaults

### Example

```typescript
const env = arkenv({
ALLOWED_ORIGINS: type("string[]").default(() => ["localhost"]),
FEATURE_FLAGS: type("string[]").default(() => []),
PORT: "number.port",
});
```
3 changes: 3 additions & 0 deletions apps/playgrounds/node/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HOST=localhost
PORT=3000
NODE_ENV=development
6 changes: 4 additions & 2 deletions apps/playgrounds/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import arkenv from "arkenv";
import arkenv, { type } from "arkenv";

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

// Automatically validate and parse process.env
// TypeScript knows the ✨exact✨ types!
const host = env.HOST;
const port = env.PORT;
const nodeEnv = env.NODE_ENV;
const allowedOrigins = env.ALLOWED_ORIGINS;

console.log({ host, port, nodeEnv });
console.log({ host, port, nodeEnv, allowedOrigins });

export default env;
6 changes: 5 additions & 1 deletion apps/www/content/docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ description: Let's get you started with a few simple steps.
Add a schema to make your environment variables **validated** and **typesafe**:

```ts title="config/env.ts" twoslash
import arkenv from 'arkenv';
import arkenv, { type } from 'arkenv';

export const env = arkenv({
// Built-in validators
Expand All @@ -48,6 +48,10 @@ description: Let's get you started with a few simple steps.
// Optional variables with defaults
LOG_LEVEL: "'debug' | 'info' | 'warn' | 'error' = 'info'",

// Array defaults using type function
ALLOWED_ORIGINS: type("string[]").default(() => ["localhost"]),
FEATURE_FLAGS: type("string[]").default(() => []),

// Optional environment variable
"API_KEY?": 'string'
});
Expand Down
44 changes: 44 additions & 0 deletions packages/arkenv/src/array-defaults-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import arkenv, { type } from "./index";

describe("arkenv array defaults", () => {
it("should work with the exact syntax from the GitHub issue", () => {
// This is the exact code from the GitHub issue that should now work
const Thing = arkenv({
array: type("number.integer[]").default(() => []),
});

expect(Thing.array).toEqual([]);
});

it("should work with complex array defaults", () => {
const env = arkenv(
{
ALLOWED_HOSTS: type("string[]").default(() => [
"localhost",
"127.0.0.1",
]),
FEATURE_FLAGS: type("string[]").default(() => []),
PORTS: type("number[]").default(() => [3000, 8080]),
},
{},
);

expect(env.ALLOWED_HOSTS).toEqual(["localhost", "127.0.0.1"]);
expect(env.FEATURE_FLAGS).toEqual([]);
expect(env.PORTS).toEqual([3000, 8080]);
});

it("should support arrays with defaults and environment overrides", () => {
const env = arkenv(
{
NUMBERS: type("number[]").default(() => [1, 2, 3]),
},
{
NUMBERS: [4, 5, 6], // Non-string environment for testing
},
);

expect(env.NUMBERS).toEqual([4, 5, 6]);
});
});
26 changes: 26 additions & 0 deletions packages/arkenv/src/create-env.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { styleText } from "node:util";
import { describe, expect, it } from "vitest";
import { createEnv } from "./create-env";
import { type } from "./type";
import { indent } from "./utils";

/**
Expand Down Expand Up @@ -83,4 +84,29 @@ describe("env", () => {

expect(TEST_STRING).toBe("hello");
});

it("should support array types with default values", () => {
const env = createEnv(
{
NUMBERS: type("number[]").default(() => [1, 2, 3]),
STRINGS: type("string[]").default(() => ["a", "b"]),
},
{},
);

expect(env.NUMBERS).toEqual([1, 2, 3]);
expect(env.STRINGS).toEqual(["a", "b"]);
});

it("should support array types with defaults when no environment value provided", () => {
// Test default value usage when environment variable is not set
const env = createEnv(
{
NUMBERS: type("number[]").default(() => [1, 2, 3]),
},
{},
);

expect(env.NUMBERS).toEqual([1, 2, 3]);
});
});
7 changes: 6 additions & 1 deletion packages/arkenv/src/create-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ type RuntimeEnvironment = Record<string, string | undefined>;

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

/**
* 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 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, string | undefined>>(
export function createEnv<const T extends Record<string, unknown>>(
def: EnvSchema<T>,
env: RuntimeEnvironment = process.env,
): distill.Out<type.infer<T, (typeof $)["t"]>> {
Expand Down
10 changes: 10 additions & 0 deletions packages/arkenv/src/type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,14 @@ describe("type", () => {
expect(result.NUMBER_VALUE).toBe(42);
expect(result.BOOLEAN_VALUE).toBe(true);
});

it("should work with array defaults using type function", () => {
// This demonstrates the specific issue mentioned in the GitHub issue
const envType = type({
array: type("number.integer[]").default(() => []),
});

const result = envType.assert({});
expect(result.array).toEqual([]);
});
});
Loading