-
Notifications
You must be signed in to change notification settings - Fork 5
Fix "Return types are not inferred in standard mode" #758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yamcodes
merged 12 commits into
main
from
return-types-are-not-inferred-in-standard-mode
Jan 21, 2026
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0c5eff8
fix: correct type inference for standard mode schemas in `createEnv` …
yamcodes b2f321d
docs(validator-mode): update validator-mode purpose
yamcodes a3d7d14
feat: Migrate Vite React example to use Zod for schema definition ins…
yamcodes 364d906
docs(validator-mode): remove outdated purpose sections
yamcodes 3a2ae80
feat(create-env): add standard validator support
yamcodes 61f6d6a
fix(arkenv): improve standard mode type inference
yamcodes a2cbd80
docs: complete standard mode inference validation
yamcodes 0055f1f
refactor: improve createEnv type inference
yamcodes caa5a9d
docs(inference): fix Zod inference type description
yamcodes 54f4e53
add changeset
yamcodes d7f0ab6
fix: Improve Standard Schema type inference
yamcodes acbed78
refactor(plugins): improve env variable type safety
yamcodes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "arkenv": patch | ||
| --- | ||
|
|
||
| #### Fix Standard Schema type inference | ||
|
|
||
| Fixed type inference when using `validator: "standard"` mode. The `env` object now correctly infers types from Standard Schema validators (Zod, Valibot, etc.) instead of wrapping them in ArkType-specific types like `distill.Out`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| # Design: Standard Mode Type Inference | ||
|
|
||
| ## Context | ||
| `createEnv` is currently defined with several overloads. The most common one is: | ||
|
|
||
| ```ts | ||
| export function createEnv<const T extends SchemaShape>( | ||
| def: EnvSchema<T>, | ||
| config?: ArkEnvConfig, | ||
| ): distill.Out<at.infer<T, $>>; | ||
| ``` | ||
|
|
||
| `EnvSchema<T>` is defined as `at.validate<def, $>`. This is an ArkType-specific type that ensures the definition is valid for ArkType. | ||
|
|
||
| When `config.validator` is `"standard"`, we want a completely different signature. | ||
|
|
||
| ## Proposed Overloads | ||
|
|
||
| We should split the overloads based on the `validator` property in `ArkEnvConfig`. | ||
|
|
||
| ### 1. Standard Mode Overload | ||
| When `validator` is explicitly `"standard"`. | ||
|
|
||
| ```ts | ||
| export function createEnv<const T extends Record<string, StandardSchemaV1>>( | ||
| def: T, | ||
| config: ArkEnvConfig & { validator: "standard" }, | ||
| ): { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }; | ||
| ``` | ||
|
|
||
| ### 2. ArkType Mode Overload | ||
| When `validator` is `"arktype"` or omitted. | ||
|
|
||
| ```ts | ||
| export function createEnv<const T extends SchemaShape>( | ||
| def: EnvSchema<T>, | ||
| config?: ArkEnvConfig & { validator?: "arktype" }, | ||
| ): distill.Out<at.infer<T, $>>; | ||
| ``` | ||
|
|
||
| ## Complexity: Optional Config | ||
| The main challenge is that `config` is optional. If it's omitted, it defaults to ArkType mode. | ||
|
|
||
| If `config` is omitted, `validator: "standard"` is definitely not there, so it should fall back to ArkType. | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| ### Handling `at.validate` and `distill.Out` | ||
| If ArkType is NOT installed, these types will error. However, since `arktype` is a peer dependency of `arkenv`, it's usually present in the environment where `arkenv` is used for development, even if the user wants to avoid it at runtime. | ||
|
|
||
| Wait, if the user explicitly wants to use ArkEnv *without* ArkType, they might not even have it in `devDependencies`. In that case, `arkenv`'s own source code will fail to compile if it imports from `arktype`. | ||
|
|
||
| However, `arkenv` is a library. Its types are shipped in `dist/`. | ||
|
|
||
| When a user uses `arkenv`: | ||
| - If they have `validator: "standard"`, they shouldn't need `arktype` types. | ||
| - If they DON'T have `arktype` installed, but `arkenv` exports types that depend on `arktype`, they might get errors from their build tool (like `tsc`) saying `arktype` not found. | ||
|
|
||
| To solve this properly, we might need to make the ArkType-specific return types conditional or use a "soft" version of them that doesn't hard-fail if `arktype` is missing (but typically, if you're using ArkType mode, you MUST have it). | ||
|
|
||
| Actually, the user said: "it obviously happens because the return types of createEnv are all arktype based, but in recent changes we've made it so that arktype is not required anymore." | ||
|
|
||
| If `arktype` is not required, then `arkenv` types must be usable without it. | ||
|
|
||
| Let's look at how `EnvSchema` is defined in `packages/arkenv/src/create-env.ts`: | ||
| ```ts | ||
| export type EnvSchema<def> = at.validate<def, $>; | ||
| ``` | ||
|
|
||
| If we want this to be "soft", we can do something like: | ||
| ```ts | ||
| export type EnvSchema<def> = at extends never ? def : at.validate<def, $>; | ||
| ``` | ||
| But `at` is an import. | ||
|
|
||
| Instead, we can use a helper type that checks if `at.validate` is available. | ||
|
|
||
| Actually, the easiest way to fix the inference specifically for Standard mode is to ensure the Standard mode overload comes first and is specific enough. | ||
|
|
||
| ### Overload Order | ||
| The TypeScript compiler picks the first matching overload. | ||
|
|
||
| ```ts | ||
| // 1. Standard Mode (Strict config) | ||
| export function createEnv<const T extends Record<string, StandardSchemaV1>>( | ||
| def: T, | ||
| config: ArkEnvConfig & { validator: "standard" }, | ||
| ): { [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> }; | ||
|
|
||
| // 2. ArkType Mode (Default or explicit) | ||
| export function createEnv<const T extends SchemaShape>( | ||
| def: EnvSchema<T>, | ||
| config?: ArkEnvConfig & { validator?: "arktype" }, | ||
| ): distill.Out<at.infer<T, $>>; | ||
| ``` | ||
|
|
||
| Wait, `EnvSchema<T>` might match a Standard Schema object too (since it's `Record<string, unknown>`-ish). | ||
|
|
||
| We need to make sure `EnvSchema<T>` doesn't swallow Standard Schema objects if `validator: "standard"` is passed. | ||
|
|
||
| ## Standard Schema Inference Refinement | ||
| Standard Schema 1.0 requires `~standard` property. | ||
| We can use this to differentiate. | ||
|
|
||
| ```ts | ||
| type StandardSchemaShape = Record<string, StandardSchemaV1>; | ||
|
|
||
| type InferredStandard<T extends StandardSchemaShape> = { | ||
| [K in keyof T]: StandardSchemaV1.InferOutput<T[K]> | ||
| }; | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Change: Fix Standard Mode Type Inference | ||
|
|
||
| ## Context | ||
| In `arkenv` v0.9.0, we introduced a `validator` mode to allow using ArkEnv without ArkType by leveraging the Standard Schema 1.0 specification. While this works at runtime, it is currently broken at compile-time: `createEnv` always returns an object inferred using ArkType's type system, even when `validator: "standard"` is specified. | ||
|
|
||
| This leads to two problems: | ||
| 1. If ArkType is not installed, the type-level imports and utility types (like `distill.Out` and `at.infer`) might fail or behave unpredictably. | ||
| 2. The inferred type is not correct because it relies on ArkType's understanding of the schema, which might differ from a Standard Schema validator's understanding (e.g., Zod, Valibot). | ||
|
|
||
| ## Why | ||
| Standard Schema is a universal standard for validation libraries. Many users want to use ArkEnv's simple API with their existing Zod or Valibot schemas without being forced to install or use ArkType for type inference. | ||
|
|
||
| ## High-Level Idea | ||
| Refactor `createEnv` overloads to use conditional types or separate overloads that switch the return type based on the `validator` configuration. | ||
|
|
||
| - If `validator: "standard"`, use `StandardSchemaV1.InferOutput` for each key in the schema. | ||
| - If `validator: "arktype"` (or default), continue using ArkType-based inference. | ||
|
|
||
| ## What Changes | ||
|
|
||
| ### 1. `createEnv` Overloads | ||
| We will update `createEnv` to correctly dispatch to the appropriate inference engine at the type level. | ||
|
|
||
| ### 2. Centralized Standard Schema Types | ||
| We already have `packages/internal/types/src/standard-schema.ts`. We will use the `InferOutput` utility from this file. | ||
|
|
||
| ## Explicit Non-Goals | ||
| - Changing the runtime validation logic (already implemented). | ||
| - Introducing complex auto-detection of schemas (we stick to the explicit `validator` flag). | ||
|
|
||
| ## Design Principle | ||
| **Type Safety without Vendor Lock-in.** ArkEnv should provide first-class type safety for both ArkType and Standard Schema users, respecting the inference rules of each. |
16 changes: 16 additions & 0 deletions
16
openspec/changes/fix-standard-mode-inference/specs/inference/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Standard Mode Inference Specification | ||
|
|
||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Standard Mode Type Inference | ||
| When `createEnv` is used with `validator: "standard"`, the returned object MUST have types inferred from the Standard Schema validators. | ||
|
|
||
| #### Scenario: Zod inference in Standard Mode | ||
| - **GIVEN** a schema object containing Zod validators (which implement Standard Schema) | ||
| - **WHEN** `createEnv` is called with `validator: "standard"` | ||
| - **THEN** the returned object MUST have types exactly matching the Zod output types | ||
| - **AND** it MUST NOT be wrapped in ArkType-specific inference types like `distill.Out` | ||
|
|
||
| #### Scenario: Inferred types are usable without ArkType | ||
| - **GIVEN** `validator: "standard"` is used | ||
| - **THEN** types of the returned environment object MUST NOT depend on ArkType types at the consumer level |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| ## 1. Spec & Docs | ||
| - [x] 1.1 Add requirement for correct type inference in `openspec/changes/fix-standard-mode-inference/specs/inference/spec.md`. | ||
|
|
||
| ## 2. Type Implementation | ||
| - [x] 2.1 Update `ArkEnvConfig` to be more precise about `validator` types (literals). | ||
| - [x] 2.2 Refactor `createEnv` overloads in `packages/arkenv/src/create-env.ts` to support Standard Schema inference. | ||
| - [x] 2.3 Fix the return cast in the implementation of `createEnv` for `standard` mode. | ||
|
|
||
| ## 3. Validation | ||
| - [x] 3.1 Verify that `examples/without-arktype/src/index.ts` now has correct type inference (can be checked by hovering or running typecheck). | ||
| - [x] 3.2 Add a new test case in `packages/arkenv` specifically for type-level inference of standard mode. | ||
| - [x] 3.3 Run `openspec validate fix-standard-mode-inference --strict`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { describe, expect, expectTypeOf, it, vi } from "vitest"; | ||
| import { createEnv } from "./create-env.ts"; | ||
|
|
||
| // Mock Standard Schema validators for testing | ||
| const createMockStandardSchema = <TOutput>(outputValue: TOutput) => ({ | ||
| "~standard": { | ||
| version: 1 as const, | ||
| vendor: "mock", | ||
| types: {} as { input: unknown; output: TOutput }, | ||
| validate: (value: unknown) => ({ value: outputValue }), | ||
| }, | ||
| }); | ||
|
|
||
| describe("Standard Mode Type Inference", () => { | ||
| it("should infer correct types from Standard Schema validators", () => { | ||
| vi.stubEnv("STRING_VAR", "test"); | ||
| vi.stubEnv("NUMBER_VAR", "42"); | ||
| vi.stubEnv("BOOLEAN_VAR", "true"); | ||
|
|
||
| const env = createEnv( | ||
| { | ||
| STRING_VAR: createMockStandardSchema("test-string"), | ||
| NUMBER_VAR: createMockStandardSchema(123), | ||
| BOOLEAN_VAR: createMockStandardSchema(true), | ||
| }, | ||
| { validator: "standard" }, | ||
| ); | ||
|
|
||
| // Type-level assertions | ||
| expectTypeOf(env.STRING_VAR).toBeString(); | ||
| expectTypeOf(env.NUMBER_VAR).toBeNumber(); | ||
| expectTypeOf(env.BOOLEAN_VAR).toBeBoolean(); | ||
|
|
||
| // Runtime assertions | ||
| expect(env.STRING_VAR).toBe("test-string"); | ||
| expect(env.NUMBER_VAR).toBe(123); | ||
| expect(env.BOOLEAN_VAR).toBe(true); | ||
|
|
||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it("should not have ArkType-specific types in standard mode", () => { | ||
| vi.stubEnv("TEST_VAR", "value"); | ||
|
|
||
| const env = createEnv( | ||
| { | ||
| TEST_VAR: createMockStandardSchema("output"), | ||
| }, | ||
| { validator: "standard" }, | ||
| ); | ||
|
|
||
| // Verify the type is a plain string, not wrapped in ArkType types | ||
| expectTypeOf(env.TEST_VAR).toBeString(); | ||
|
|
||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it("should correctly infer object types from Standard Schema", () => { | ||
| vi.stubEnv("OBJECT_VAR", "{}"); | ||
|
|
||
| type ExpectedOutput = { foo: string; bar: number }; | ||
| const env = createEnv( | ||
| { | ||
| OBJECT_VAR: createMockStandardSchema<ExpectedOutput>({ | ||
| foo: "test", | ||
| bar: 42, | ||
| }), | ||
| }, | ||
| { validator: "standard" }, | ||
| ); | ||
|
|
||
| expectTypeOf(env.OBJECT_VAR).toEqualTypeOf<ExpectedOutput>(); | ||
| expect(env.OBJECT_VAR).toEqual({ foo: "test", bar: 42 }); | ||
|
|
||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it("should throw error when ArkType DSL strings are used in standard mode", () => { | ||
| expect(() => | ||
| createEnv( | ||
| { | ||
| TEST_VAR: "string", | ||
| } as any, | ||
| { validator: "standard" }, | ||
| ), | ||
| ).toThrow(/ArkType DSL strings are not supported in "standard" mode/); | ||
| }); | ||
|
|
||
| it("should throw error when non-standard validators are used", () => { | ||
| expect(() => | ||
| createEnv( | ||
| { | ||
| TEST_VAR: { notAStandardSchema: true }, | ||
| } as any, | ||
| { validator: "standard" }, | ||
| ), | ||
| ).toThrow(/Invalid validator: expected a Standard Schema 1.0 validator/); | ||
| }); | ||
|
|
||
| it("should maintain type safety with multiple validators", () => { | ||
| vi.stubEnv("VAR1", "a"); | ||
| vi.stubEnv("VAR2", "b"); | ||
| vi.stubEnv("VAR3", "c"); | ||
|
|
||
| const env = createEnv( | ||
| { | ||
| VAR1: createMockStandardSchema("string-output"), | ||
| VAR2: createMockStandardSchema(999), | ||
| VAR3: createMockStandardSchema({ nested: "object" }), | ||
| }, | ||
| { validator: "standard" }, | ||
| ); | ||
|
|
||
| expectTypeOf(env.VAR1).toBeString(); | ||
| expectTypeOf(env.VAR2).toBeNumber(); | ||
| expectTypeOf(env.VAR3).toEqualTypeOf<{ nested: string }>(); | ||
|
|
||
| vi.unstubAllEnvs(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.