env-validated 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +833 -0
  3. package/dist/adapters/arktype.cjs +38 -0
  4. package/dist/adapters/arktype.d.cts +1 -0
  5. package/dist/adapters/arktype.d.ts +1 -0
  6. package/dist/adapters/arktype.js +11 -0
  7. package/dist/adapters/effect.cjs +38 -0
  8. package/dist/adapters/effect.d.cts +1 -0
  9. package/dist/adapters/effect.d.ts +1 -0
  10. package/dist/adapters/effect.js +11 -0
  11. package/dist/adapters/joi.cjs +57 -0
  12. package/dist/adapters/joi.d.cts +55 -0
  13. package/dist/adapters/joi.d.ts +55 -0
  14. package/dist/adapters/joi.js +35 -0
  15. package/dist/adapters/runtypes.cjs +38 -0
  16. package/dist/adapters/runtypes.d.cts +1 -0
  17. package/dist/adapters/runtypes.d.ts +1 -0
  18. package/dist/adapters/runtypes.js +11 -0
  19. package/dist/adapters/superstruct.cjs +38 -0
  20. package/dist/adapters/superstruct.d.cts +1 -0
  21. package/dist/adapters/superstruct.d.ts +1 -0
  22. package/dist/adapters/superstruct.js +11 -0
  23. package/dist/adapters/typebox.cjs +38 -0
  24. package/dist/adapters/typebox.d.cts +1 -0
  25. package/dist/adapters/typebox.d.ts +1 -0
  26. package/dist/adapters/typebox.js +11 -0
  27. package/dist/adapters/valibot.cjs +38 -0
  28. package/dist/adapters/valibot.d.cts +1 -0
  29. package/dist/adapters/valibot.d.ts +1 -0
  30. package/dist/adapters/valibot.js +11 -0
  31. package/dist/adapters/yup.cjs +38 -0
  32. package/dist/adapters/yup.d.cts +1 -0
  33. package/dist/adapters/yup.d.ts +1 -0
  34. package/dist/adapters/yup.js +11 -0
  35. package/dist/adapters/zod.cjs +38 -0
  36. package/dist/adapters/zod.d.cts +1 -0
  37. package/dist/adapters/zod.d.ts +1 -0
  38. package/dist/adapters/zod.js +11 -0
  39. package/dist/effect-DIhhk_ck.d.cts +222 -0
  40. package/dist/effect-DIhhk_ck.d.ts +222 -0
  41. package/dist/index.cjs +737 -0
  42. package/dist/index.d.cts +8 -0
  43. package/dist/index.d.ts +8 -0
  44. package/dist/index.js +714 -0
  45. package/package.json +215 -0
package/README.md ADDED
@@ -0,0 +1,833 @@
1
+ # env-validated
2
+
3
+ **Framework-agnostic environment variable validation with pluggable validators, zero dependencies, and full TypeScript inference.**
4
+
5
+ Validate your env vars at startup. Get a fully typed, frozen config object. Never touch `process.env` directly again.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/env-validated.svg)](https://www.npmjs.com/package/env-validated)
8
+ [![license](https://img.shields.io/npm/l/env-validated.svg)](https://github.com/husainkorasawala/env-validated/blob/main/LICENSE)
9
+
10
+ ---
11
+
12
+ ## Why env-validated?
13
+
14
+ Every project solves environment validation from scratch. Existing solutions are either locked to a specific validator (t3-env requires Zod), lack TypeScript inference (envalid), or can't work across different runtimes.
15
+
16
+ env-validated fills all three gaps at once:
17
+
18
+ - **Zero dependencies** — the core package has no runtime dependencies
19
+ - **Full TypeScript inference** — no type casting, no manual type declarations
20
+ - **Pluggable validators** — use Zod, Joi, Yup, Valibot, or 5 other libraries via auto-detected adapters
21
+ - **Object schemas** — pass a pre-defined `z.object()`, `yup.object()`, etc. directly
22
+ - **Any runtime** — Node.js, Vite, Next.js, Deno, Cloudflare Workers, CLI tools
23
+ - **Mix and match** — use different validators per field in the same schema
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install env-validated
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```ts
34
+ import { createEnv } from 'env-validated'
35
+
36
+ export const env = createEnv({
37
+ schema: {
38
+ API_URL: { type: 'url', required: true },
39
+ PORT: { type: 'number', default: 3000 },
40
+ NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'] as const },
41
+ ENABLE_FEATURE: { type: 'boolean', default: false },
42
+ SECRET_KEY: { type: 'string', required: true, minLength: 32 },
43
+ }
44
+ })
45
+
46
+ // Fully typed - no casting needed:
47
+ env.PORT // number
48
+ env.ENABLE_FEATURE // boolean
49
+ env.NODE_ENV // 'development' | 'production' | 'test'
50
+ env.API_URL // string
51
+ ```
52
+
53
+ If any variable is missing or invalid, env-validated throws a single clear error listing every problem:
54
+
55
+ ```
56
+ [env-validated] Missing or invalid environment variables:
57
+ ✖ API_URL — required, was not set
58
+ ✖ SECRET_KEY — must be at least 32 characters (got 12)
59
+ ✖ NODE_ENV — must be one of: development, production, test (got "staging")
60
+ ✖ PORT — must be a number (got "not-a-number")
61
+
62
+ Fix these before starting the app.
63
+ ```
64
+
65
+ ## Built-in Types
66
+
67
+ env-validated ships with lightweight validators for the most common types:
68
+
69
+ | Type | Output Type | Options | Description |
70
+ |------|------------|---------|-------------|
71
+ | `string` | `string` | `required`, `minLength`, `maxLength`, `pattern` | Plain string validation |
72
+ | `number` | `number` | `required`, `min`, `max`, `default` | Coerces string to number |
73
+ | `boolean` | `boolean` | `required`, `default` | Parses `true`/`false`/`1`/`0`/`yes`/`no` |
74
+ | `url` | `string` | `required`, `default` | Validates URL format |
75
+ | `enum` | union type | `values`, `required`, `default` | Must match one of `values[]` |
76
+ | `json` | `unknown` | `required`, `default` | Parses and validates JSON strings |
77
+ | `port` | `number` | `required`, `default` | Validates port range 1–65535 |
78
+
79
+ ```ts
80
+ const env = createEnv({
81
+ schema: {
82
+ DB_HOST: { type: 'string', default: 'localhost' },
83
+ DB_PORT: { type: 'port', default: 5432 },
84
+ LOG_JSON: { type: 'boolean', default: false },
85
+ REGION: { type: 'enum', values: ['us-east', 'eu-west'] as const },
86
+ METADATA: { type: 'json' },
87
+ }
88
+ })
89
+ ```
90
+
91
+ ## Pluggable Validators
92
+
93
+ env-validated auto-detects which validator library a schema field belongs to. No adapter imports or configuration needed — just pass the schema object directly.
94
+
95
+ ### Zod
96
+
97
+ ```bash
98
+ npm install zod
99
+ ```
100
+
101
+ ```ts
102
+ import { createEnv } from 'env-validated'
103
+ import { z } from 'zod'
104
+
105
+ const env = createEnv({
106
+ schema: {
107
+ PORT: z.coerce.number().min(1000),
108
+ TAGS: z.string().transform(s => s.split(',')),
109
+ ENV: z.enum(['dev', 'prod', 'test']),
110
+ }
111
+ })
112
+
113
+ env.PORT // number
114
+ env.TAGS // string[]
115
+ env.ENV // 'dev' | 'prod' | 'test'
116
+ ```
117
+
118
+ ### Joi
119
+
120
+ ```bash
121
+ npm install joi
122
+ ```
123
+
124
+ ```ts
125
+ import { createEnv } from 'env-validated'
126
+ import Joi from 'joi'
127
+
128
+ const env = createEnv({
129
+ schema: {
130
+ PORT: Joi.number().min(1000).required(),
131
+ NAME: Joi.string().min(3).required(),
132
+ ENV: Joi.string().valid('dev', 'prod', 'test').required(),
133
+ }
134
+ })
135
+
136
+ env.PORT // number
137
+ env.NAME // string
138
+ env.ENV // string
139
+ ```
140
+
141
+ Joi's type system carries schema types through structural matching, so `Joi.string()` infers `string`, `Joi.number()` infers `number`, and `Joi.boolean()` infers `boolean` automatically.
142
+
143
+ ### Yup
144
+
145
+ ```bash
146
+ npm install yup
147
+ ```
148
+
149
+ ```ts
150
+ import { createEnv } from 'env-validated'
151
+ import * as yup from 'yup'
152
+
153
+ const env = createEnv({
154
+ schema: {
155
+ PORT: yup.number().required().min(1000),
156
+ NAME: yup.string().required().min(3),
157
+ FLAG: yup.boolean().required(),
158
+ }
159
+ })
160
+
161
+ env.PORT // number
162
+ env.FLAG // boolean
163
+ ```
164
+
165
+ The Yup adapter pre-coerces string values to `number` or `boolean` based on the schema type, so you don't need `.transform()`.
166
+
167
+ ### Valibot
168
+
169
+ ```bash
170
+ npm install valibot
171
+ ```
172
+
173
+ ```ts
174
+ import { createEnv } from 'env-validated'
175
+ import * as v from 'valibot'
176
+
177
+ const env = createEnv({
178
+ schema: {
179
+ NAME: v.pipe(v.string(), v.minLength(3)),
180
+ ENV: v.picklist(['dev', 'prod', 'test']),
181
+ }
182
+ })
183
+
184
+ env.NAME // string
185
+ env.ENV // 'dev' | 'prod' | 'test'
186
+ ```
187
+
188
+ ### TypeBox
189
+
190
+ ```bash
191
+ npm install @sinclair/typebox
192
+ ```
193
+
194
+ ```ts
195
+ import { createEnv } from 'env-validated'
196
+ import { Type } from '@sinclair/typebox'
197
+
198
+ const env = createEnv({
199
+ schema: {
200
+ PORT: Type.Number(),
201
+ NAME: Type.String({ minLength: 3 }),
202
+ FLAG: Type.Boolean(),
203
+ }
204
+ })
205
+
206
+ env.PORT // number
207
+ env.FLAG // boolean
208
+ ```
209
+
210
+ TypeBox values are automatically coerced from strings using `Value.Convert`.
211
+
212
+ ### ArkType
213
+
214
+ ```bash
215
+ npm install arktype
216
+ ```
217
+
218
+ ```ts
219
+ import { createEnv } from 'env-validated'
220
+ import { type } from 'arktype'
221
+
222
+ const env = createEnv({
223
+ schema: {
224
+ NAME: type('string >= 3'),
225
+ ENV: type("'dev' | 'prod' | 'test'"),
226
+ }
227
+ })
228
+ ```
229
+
230
+ ### Superstruct
231
+
232
+ ```bash
233
+ npm install superstruct
234
+ ```
235
+
236
+ ```ts
237
+ import { createEnv } from 'env-validated'
238
+ import { string, size, enums } from 'superstruct'
239
+
240
+ const env = createEnv({
241
+ schema: {
242
+ NAME: size(string(), 3, 100),
243
+ ENV: enums(['dev', 'prod', 'test']),
244
+ }
245
+ })
246
+ ```
247
+
248
+ ### Runtypes
249
+
250
+ ```bash
251
+ npm install runtypes
252
+ ```
253
+
254
+ ```ts
255
+ import { createEnv } from 'env-validated'
256
+ import { String, Union, Literal } from 'runtypes'
257
+
258
+ const env = createEnv({
259
+ schema: {
260
+ NAME: String.withConstraint(s => s.length >= 3 || 'too short'),
261
+ ENV: Union(Literal('dev'), Literal('prod'), Literal('test')),
262
+ }
263
+ })
264
+ ```
265
+
266
+ ### Effect Schema
267
+
268
+ ```bash
269
+ npm install effect
270
+ ```
271
+
272
+ For older setups that only use the standalone package, install `@effect/schema` instead; env-validated supports both as optional peers.
273
+
274
+ ```ts
275
+ import { createEnv } from 'env-validated'
276
+ import { Schema } from 'effect'
277
+
278
+ const env = createEnv({
279
+ schema: {
280
+ NAME: Schema.String.pipe(Schema.minLength(3)),
281
+ ENV: Schema.Literal('dev', 'prod', 'test'),
282
+ }
283
+ })
284
+ ```
285
+
286
+ ## Object-Level Schemas
287
+
288
+ If you already have a pre-defined object schema from any validator library, you can pass it directly to `createEnv` instead of defining fields individually. Types are inferred automatically.
289
+
290
+ ### Zod
291
+
292
+ ```ts
293
+ import { z } from 'zod'
294
+
295
+ const envSchema = z.object({
296
+ HOST: z.string(),
297
+ PORT: z.coerce.number(),
298
+ DEBUG: z.coerce.boolean(),
299
+ })
300
+
301
+ const env = createEnv({ schema: envSchema })
302
+
303
+ env.HOST // string
304
+ env.PORT // number
305
+ env.DEBUG // boolean
306
+ ```
307
+
308
+ ### Yup
309
+
310
+ ```ts
311
+ import * as yup from 'yup'
312
+
313
+ const envSchema = yup.object({
314
+ HOST: yup.string().required(),
315
+ PORT: yup.number().required(),
316
+ })
317
+
318
+ const env = createEnv({ schema: envSchema })
319
+
320
+ env.HOST // string
321
+ env.PORT // number
322
+ ```
323
+
324
+ ### Valibot
325
+
326
+ ```ts
327
+ import * as v from 'valibot'
328
+
329
+ const envSchema = v.object({
330
+ HOST: v.string(),
331
+ PORT: v.string(),
332
+ })
333
+
334
+ const env = createEnv({ schema: envSchema })
335
+
336
+ env.HOST // string
337
+ env.PORT // string
338
+ ```
339
+
340
+ ### TypeBox
341
+
342
+ ```ts
343
+ import { Type } from '@sinclair/typebox'
344
+
345
+ const envSchema = Type.Object({
346
+ HOST: Type.String(),
347
+ PORT: Type.Number(),
348
+ })
349
+
350
+ const env = createEnv({ schema: envSchema })
351
+
352
+ env.HOST // string
353
+ env.PORT // number
354
+ ```
355
+
356
+ ### ArkType
357
+
358
+ ```ts
359
+ import { type } from 'arktype'
360
+
361
+ const envSchema = type({
362
+ HOST: 'string',
363
+ PORT: 'string',
364
+ })
365
+
366
+ const env = createEnv({ schema: envSchema })
367
+
368
+ env.HOST // string
369
+ env.PORT // string
370
+ ```
371
+
372
+ ### Superstruct
373
+
374
+ ```ts
375
+ import { object, string } from 'superstruct'
376
+
377
+ const envSchema = object({
378
+ HOST: string(),
379
+ PORT: string(),
380
+ })
381
+
382
+ const env = createEnv({ schema: envSchema })
383
+
384
+ env.HOST // string
385
+ env.PORT // string
386
+ ```
387
+
388
+ ### Runtypes
389
+
390
+ ```ts
391
+ import { Object, String } from 'runtypes'
392
+
393
+ const envSchema = Object({
394
+ HOST: String,
395
+ PORT: String,
396
+ })
397
+
398
+ const env = createEnv({ schema: envSchema })
399
+
400
+ env.HOST // string
401
+ env.PORT // string
402
+ ```
403
+
404
+ ### Effect Schema
405
+
406
+ ```ts
407
+ import { Schema } from 'effect'
408
+
409
+ const envSchema = Schema.Struct({
410
+ HOST: Schema.String,
411
+ PORT: Schema.String,
412
+ })
413
+
414
+ const env = createEnv({ schema: envSchema })
415
+
416
+ env.HOST // string
417
+ env.PORT // string
418
+ ```
419
+
420
+ ### Joi
421
+
422
+ Joi's type definitions don't preserve inner schema types in `Joi.object()`. Use the `joiObject()` wrapper for full type inference — no manual annotations needed:
423
+
424
+ ```ts
425
+ import Joi from 'joi'
426
+ import { joiObject } from 'env-validated/adapters/joi'
427
+
428
+ const env = createEnv({
429
+ schema: joiObject({
430
+ HOST: Joi.string().required(),
431
+ PORT: Joi.number().min(1000).required(),
432
+ DEBUG: Joi.boolean().default(false),
433
+ }),
434
+ })
435
+
436
+ env.HOST // string
437
+ env.PORT // number
438
+ env.DEBUG // boolean
439
+ ```
440
+
441
+ > **Why the wrapper?** `Joi.object<TSchema = any>()` erases inner schema types at the TypeScript level &mdash; this is a Joi limitation. `joiObject()` preserves them by attaching a phantom type that the inference engine reads. No manual type annotations are required; it infers `string`, `number`, `boolean`, and `Date` from the Joi schema methods automatically.
442
+
443
+ ## Mixing Validators
444
+
445
+ You can use different validators for different fields in the same schema:
446
+
447
+ ```ts
448
+ import { createEnv } from 'env-validated'
449
+ import { z } from 'zod'
450
+ import Joi from 'joi'
451
+
452
+ const env = createEnv({
453
+ schema: {
454
+ // Zod
455
+ PORT: z.coerce.number().min(1000),
456
+
457
+ // Joi
458
+ API_KEY: Joi.string().min(32).required(),
459
+
460
+ // Built-in
461
+ NODE_ENV: { type: 'enum', values: ['dev', 'prod'] as const },
462
+
463
+ // Custom validate function
464
+ ALLOWED_IPS: {
465
+ validate: (val) => {
466
+ if (!val) return { success: false, error: 'required' }
467
+ const ips = val.split(',').map(s => s.trim())
468
+ const valid = ips.every(ip => /^\d{1,3}(\.\d{1,3}){3}$/.test(ip))
469
+ return valid
470
+ ? { success: true, value: ips }
471
+ : { success: false, error: 'Must be comma-separated IPs' }
472
+ }
473
+ },
474
+ }
475
+ })
476
+ ```
477
+
478
+ ## Custom Validate Function
479
+
480
+ For one-off fields that don't need a full library, pass an object with a `validate` function:
481
+
482
+ ```ts
483
+ const env = createEnv({
484
+ schema: {
485
+ CSV_LIST: {
486
+ validate: (val) => {
487
+ if (!val) return { success: false, error: 'required' }
488
+ const items = val.split(',').map(s => s.trim())
489
+ return { success: true, value: items }
490
+ }
491
+ }
492
+ }
493
+ })
494
+ // env.CSV_LIST is inferred as string[]
495
+ ```
496
+
497
+ The `validate` function receives the raw string (or `undefined` if not set) and must return either:
498
+ - `{ success: true, value: T }` on success
499
+ - `{ success: false, error: string }` on failure
500
+
501
+ The return type `T` is inferred automatically &mdash; no manual type annotations needed.
502
+
503
+ ## Framework Guides
504
+
505
+ ### Node.js / Express / Fastify
506
+
507
+ No configuration needed. env-validated reads `process.env` by default.
508
+
509
+ ```ts
510
+ // src/env.ts
511
+ import { createEnv } from 'env-validated'
512
+
513
+ export const env = createEnv({
514
+ schema: {
515
+ PORT: { type: 'port', default: 3000 },
516
+ DATABASE_URL: { type: 'url' },
517
+ NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'] as const },
518
+ }
519
+ })
520
+
521
+ // src/app.ts
522
+ import { env } from './env.js'
523
+ app.listen(env.PORT)
524
+ ```
525
+
526
+ ### Next.js
527
+
528
+ ```ts
529
+ // src/env.ts
530
+ import { createEnv } from 'env-validated'
531
+
532
+ // Server-side env vars
533
+ export const env = createEnv({
534
+ schema: {
535
+ DATABASE_URL: { type: 'url' },
536
+ API_SECRET: { type: 'string', minLength: 32 },
537
+ }
538
+ })
539
+
540
+ // Client-side env vars (with prefix stripping)
541
+ export const clientEnv = createEnv(
542
+ {
543
+ schema: {
544
+ API_URL: { type: 'url' },
545
+ APP_NAME: { type: 'string' },
546
+ }
547
+ },
548
+ { source: process.env, prefix: 'NEXT_PUBLIC_' }
549
+ )
550
+ ```
551
+
552
+ ### Vite
553
+
554
+ ```ts
555
+ import { createEnv } from 'env-validated'
556
+
557
+ export const env = createEnv(
558
+ {
559
+ schema: {
560
+ API_URL: { type: 'url' },
561
+ DEBUG: { type: 'boolean', default: false },
562
+ }
563
+ },
564
+ { source: import.meta.env, prefix: 'VITE_' }
565
+ )
566
+ ```
567
+
568
+ ### Remix
569
+
570
+ ```ts
571
+ // app/env.server.ts
572
+ import { createEnv } from 'env-validated'
573
+
574
+ export const env = createEnv({
575
+ schema: {
576
+ DATABASE_URL: { type: 'url' },
577
+ SESSION_SECRET: { type: 'string', minLength: 32 },
578
+ }
579
+ })
580
+ ```
581
+
582
+ ### Cloudflare Workers
583
+
584
+ ```ts
585
+ // In your fetch handler
586
+ export default {
587
+ async fetch(request: Request, cfEnv: Env) {
588
+ const env = createEnv(
589
+ {
590
+ schema: {
591
+ API_KEY: { type: 'string', minLength: 16 },
592
+ REGION: { type: 'enum', values: ['us', 'eu'] as const },
593
+ }
594
+ },
595
+ { source: cfEnv as Record<string, string> }
596
+ )
597
+
598
+ // env.API_KEY is typed and validated
599
+ }
600
+ }
601
+ ```
602
+
603
+ ### Deno
604
+
605
+ ```ts
606
+ import { createEnv } from 'env-validated'
607
+
608
+ const env = createEnv(
609
+ {
610
+ schema: {
611
+ PORT: { type: 'port', default: 8000 },
612
+ ENV: { type: 'enum', values: ['dev', 'prod'] as const },
613
+ }
614
+ },
615
+ { source: Deno.env.toObject() }
616
+ )
617
+ ```
618
+
619
+ ### NestJS
620
+
621
+ ```ts
622
+ // src/env.ts
623
+ import { createEnv } from 'env-validated'
624
+
625
+ export const env = createEnv({
626
+ schema: {
627
+ PORT: { type: 'port', default: 3000 },
628
+ DATABASE_URL: { type: 'url' },
629
+ }
630
+ })
631
+
632
+ // src/app.module.ts
633
+ import { env } from './env.js'
634
+
635
+ @Module({
636
+ imports: [
637
+ TypeOrmModule.forRoot({ url: env.DATABASE_URL }),
638
+ ],
639
+ })
640
+ export class AppModule {}
641
+ ```
642
+
643
+ ### Testing (Jest / Vitest)
644
+
645
+ Pass a plain object as `source` for full isolation:
646
+
647
+ ```ts
648
+ import { createEnv } from 'env-validated'
649
+
650
+ const env = createEnv(
651
+ {
652
+ schema: {
653
+ API_URL: { type: 'url' },
654
+ PORT: { type: 'port', default: 3000 },
655
+ }
656
+ },
657
+ {
658
+ source: {
659
+ API_URL: 'https://test.example.com',
660
+ PORT: '4000',
661
+ }
662
+ }
663
+ )
664
+ ```
665
+
666
+ ## Options
667
+
668
+ | Option | Type | Default | Description |
669
+ |--------|------|---------|-------------|
670
+ | `source` | `Record<string, string \| undefined>` | `process.env` | Custom env source |
671
+ | `onError` | `'throw' \| 'warn' \| (err: EnvSafeError) => void` | `'throw'` | Error handling strategy |
672
+ | `dotenv` | `boolean \| string` | `undefined` | Auto-load `.env` file (requires `dotenv` installed) |
673
+ | `prefix` | `string` | `undefined` | Strip prefix from env keys (e.g. `'VITE_'`, `'NEXT_PUBLIC_'`) |
674
+
675
+ ### Error Handling
676
+
677
+ ```ts
678
+ // Default: throws EnvSafeError with all failures
679
+ createEnv({ schema: { ... } })
680
+
681
+ // Warn to console instead of throwing
682
+ createEnv({ schema: { ... } }, { onError: 'warn' })
683
+
684
+ // Custom handler
685
+ createEnv({ schema: { ... } }, {
686
+ onError: (err) => {
687
+ logger.error(err.message)
688
+ process.exit(1)
689
+ }
690
+ })
691
+
692
+ // Catch programmatically
693
+ import { createEnv, EnvSafeError } from 'env-validated'
694
+
695
+ try {
696
+ const env = createEnv({ schema: { ... } })
697
+ } catch (e) {
698
+ if (e instanceof EnvSafeError) {
699
+ console.log(e.errors) // [{ key: 'PORT', message: '...' }, ...]
700
+ }
701
+ }
702
+ ```
703
+
704
+ ## Secret Redaction
705
+
706
+ Variables whose names end with `_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, or `_PASS` are automatically masked in error output. Their values are never printed to the console.
707
+
708
+ ```
709
+ [env-validated] Missing or invalid environment variables:
710
+ ✖ API_KEY — must be at least 32 characters (got *****)
711
+ ✖ DB_PASSWORD — required, was not set
712
+ ```
713
+
714
+ The actual value is still validated normally.
715
+
716
+ ## TypeScript Inference
717
+
718
+ Types are inferred automatically from the schema. No type casting or manual type declarations needed.
719
+
720
+ **Built-in types:**
721
+
722
+ ```ts
723
+ const env = createEnv({
724
+ schema: {
725
+ PORT: { type: 'number', default: 3000 },
726
+ DRY_RUN: { type: 'boolean', default: false },
727
+ ENV: { type: 'enum', values: ['dev', 'prod'] as const },
728
+ }
729
+ })
730
+
731
+ env.PORT // number
732
+ env.DRY_RUN // boolean
733
+ env.ENV // 'dev' | 'prod'
734
+ ```
735
+
736
+ **External validators** &mdash; types pass through natively:
737
+
738
+ ```ts
739
+ import { z } from 'zod'
740
+
741
+ const env = createEnv({
742
+ schema: {
743
+ COUNT: z.coerce.number().int().positive(),
744
+ TAGS: z.string().transform(s => s.split(',')),
745
+ }
746
+ })
747
+
748
+ env.COUNT // number
749
+ env.TAGS // string[]
750
+ ```
751
+
752
+ **Custom validate functions** &mdash; return type is inferred:
753
+
754
+ ```ts
755
+ const env = createEnv({
756
+ schema: {
757
+ IPS: {
758
+ validate: (val) =>
759
+ val
760
+ ? { success: true as const, value: val.split(',') }
761
+ : { success: false as const, error: 'required' },
762
+ },
763
+ }
764
+ })
765
+
766
+ env.IPS // string[]
767
+ ```
768
+
769
+ **Object-level schemas** &mdash; full output type is extracted:
770
+
771
+ ```ts
772
+ import { z } from 'zod'
773
+
774
+ const env = createEnv({
775
+ schema: z.object({
776
+ HOST: z.string(),
777
+ PORT: z.coerce.number(),
778
+ })
779
+ })
780
+
781
+ env.HOST // string
782
+ env.PORT // number
783
+ ```
784
+
785
+ > **Tip:** Use `as const` on enum `values` arrays to get narrow union types instead of `string`.
786
+
787
+ ## Comparison
788
+
789
+ | Feature | t3-env | envalid | env-validated |
790
+ |---------|--------|---------|----------|
791
+ | Zod required | Yes | No | No (optional) |
792
+ | TS inference | Yes | No | Yes |
793
+ | Framework agnostic | Partial | Yes | Yes |
794
+ | Custom env source | No | No | Yes |
795
+ | Zero dependencies | No | No | Yes |
796
+ | Pluggable validators | No | No | Yes (9 adapters) |
797
+ | Object-level schemas | Partial | No | Yes |
798
+ | Mix validators per field | No | No | Yes |
799
+ | Community adapters | No | No | Yes |
800
+
801
+ ## Supported Adapters
802
+
803
+ | Library | Auto-detected | Type Inference | Object Schema |
804
+ |---------|---------------|----------------|---------------|
805
+ | [Zod](https://github.com/colinhacks/zod) | Yes | Full | `z.object()` |
806
+ | [Joi](https://github.com/hapijs/joi) | Yes | `string` / `number` / `boolean` / `Date` | Via `joiObject()` |
807
+ | [Yup](https://github.com/jquense/yup) | Yes | Full | `yup.object()` |
808
+ | [Valibot](https://github.com/fabian-hiller/valibot) | Yes | Full | `v.object()` |
809
+ | [TypeBox](https://github.com/sinclairzx81/typebox) | Yes | Full | `Type.Object()` |
810
+ | [ArkType](https://github.com/arktypeio/arktype) | Yes | Full | `type({...})` |
811
+ | [Superstruct](https://github.com/ianstormtaylor/superstruct) | Yes | Full | `object()` |
812
+ | [Runtypes](https://github.com/runtypes/runtypes) | Yes | Full | `Object()` |
813
+ | [Effect Schema](https://github.com/Effect-TS/effect) | Yes | Full | `Schema.Struct()` |
814
+
815
+ ## Community Adapters
816
+
817
+ The validator contract is a simple public interface. Anyone can publish their own adapter as a separate npm package:
818
+
819
+ ```ts
820
+ import { registerAdapter } from 'env-validated'
821
+
822
+ registerAdapter({
823
+ name: 'my-validator',
824
+ detect: (field) => /* return true if this adapter handles the field */,
825
+ validate: (field, raw, key) => {
826
+ // return { success: true, value: parsed } or { success: false, error: 'message' }
827
+ },
828
+ })
829
+ ```
830
+
831
+ ## License
832
+
833
+ [MIT](./LICENSE)