env-dictionary 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.
- package/README.md +315 -0
- package/bun.lock +25 -0
- package/index.ts +1 -0
- package/lib/dictionary.ts +59 -0
- package/lib/t.ts +47 -0
- package/lib/types.ts +21 -0
- package/package.json +12 -0
- package/tsconfig.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Dictionary
|
|
2
|
+
|
|
3
|
+
A TypeScript library for type-safe environment variable handling with runtime validation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type Safety**: Full TypeScript support with inferred types
|
|
8
|
+
- **Runtime Validation**: Validate environment variables against defined schemas
|
|
9
|
+
- **Multiple Type Support**: Built-in validators for string, number, boolean, object, array, and any types
|
|
10
|
+
- **Flexible API**: Choose between schema-based or descriptor-based approaches
|
|
11
|
+
- **Zero Dependencies**: Lightweight with no external runtime dependencies
|
|
12
|
+
- **Bun Optimized**: Built with Bun in mind, works with any TypeScript project
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the package:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Using Dictionary (Schema-based)
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Dictionary } from './lib/dictionary';
|
|
28
|
+
import { t } from './lib/t';
|
|
29
|
+
|
|
30
|
+
const config = new Dictionary({
|
|
31
|
+
env: {
|
|
32
|
+
API_KEY: 'secret',
|
|
33
|
+
PORT: 3000,
|
|
34
|
+
DEBUG: true,
|
|
35
|
+
},
|
|
36
|
+
schema: {
|
|
37
|
+
API_KEY: t.string(),
|
|
38
|
+
PORT: t.number(),
|
|
39
|
+
DEBUG: t.boolean(),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(config.values.API_KEY); // 'secret'
|
|
44
|
+
console.log(config.values.PORT); // 3000
|
|
45
|
+
console.log(config.values.DEBUG); // true
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Using DictionaryEnv (Descriptor-based)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { DictionaryEnv } from './lib/dictionary';
|
|
52
|
+
import { t } from './lib/t';
|
|
53
|
+
|
|
54
|
+
const env = new DictionaryEnv([
|
|
55
|
+
{ API_KEY: 'secret', type: t.string() },
|
|
56
|
+
{ PORT: 3000, type: t.number() },
|
|
57
|
+
{ DEBUG: true, type: t.boolean() },
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
console.log(env.env.API_KEY); // 'secret'
|
|
61
|
+
console.log(env.env.PORT); // 3000
|
|
62
|
+
console.log(env.env.DEBUG); // true
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Reference
|
|
66
|
+
|
|
67
|
+
### t
|
|
68
|
+
|
|
69
|
+
The `t` object provides type validators for schema definitions.
|
|
70
|
+
|
|
71
|
+
#### Available Types
|
|
72
|
+
|
|
73
|
+
| Type | Validator | Example |
|
|
74
|
+
|------|-----------|---------|
|
|
75
|
+
| `string` | `t.string()` | `'hello'` |
|
|
76
|
+
| `number` | `t.number()` | `42` |
|
|
77
|
+
| `boolean` | `t.boolean()` | `true` |
|
|
78
|
+
| `object` | `t.object()` | `{ key: 'value' }` |
|
|
79
|
+
| `array` | `t.array()` | `[1, 2, 3]` |
|
|
80
|
+
| `any` | `t.any()` | Any value |
|
|
81
|
+
|
|
82
|
+
### Dictionary
|
|
83
|
+
|
|
84
|
+
A class that validates environment variables against a schema.
|
|
85
|
+
|
|
86
|
+
#### Constructor
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
new Dictionary<T>(options: DictionaryOptions)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Parameters:**
|
|
93
|
+
|
|
94
|
+
- `options.env: Record<string, unknown>` - The environment variables to validate
|
|
95
|
+
- `options.schema?: Record<string, SchemaType>` - Optional schema for validation
|
|
96
|
+
|
|
97
|
+
**Properties:**
|
|
98
|
+
|
|
99
|
+
- `values: T` - The validated and typed environment variables
|
|
100
|
+
|
|
101
|
+
#### Example
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const dict = new Dictionary({
|
|
105
|
+
env: {
|
|
106
|
+
API_KEY: 'secret',
|
|
107
|
+
PORT: 3000,
|
|
108
|
+
DEBUG: true,
|
|
109
|
+
CONFIG: { url: 'https://api.example.com' },
|
|
110
|
+
ITEMS: [1, 2, 3],
|
|
111
|
+
},
|
|
112
|
+
schema: {
|
|
113
|
+
API_KEY: t.string(),
|
|
114
|
+
PORT: t.number(),
|
|
115
|
+
DEBUG: t.boolean(),
|
|
116
|
+
CONFIG: t.object(),
|
|
117
|
+
ITEMS: t.array(),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Behavior
|
|
123
|
+
|
|
124
|
+
- If no schema is provided, all environment variables are included as-is
|
|
125
|
+
- If a schema is provided, only keys defined in the schema are included
|
|
126
|
+
- Throws an error if a variable fails validation
|
|
127
|
+
- Values are automatically typed based on the schema
|
|
128
|
+
|
|
129
|
+
### DictionaryEnv
|
|
130
|
+
|
|
131
|
+
A class that creates a typed environment object from an array of descriptors.
|
|
132
|
+
|
|
133
|
+
#### Constructor
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
new DictionaryEnv<TSchema>(schema: TSchema)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Parameters:**
|
|
140
|
+
|
|
141
|
+
- `schema: TSchema[]` - Array of descriptors defining environment variables
|
|
142
|
+
|
|
143
|
+
**Properties:**
|
|
144
|
+
|
|
145
|
+
- `env: TEnv` - The typed environment object
|
|
146
|
+
|
|
147
|
+
#### Example
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const env = new DictionaryEnv([
|
|
151
|
+
{ API_KEY: 'secret', type: t.string() },
|
|
152
|
+
{ PORT: 3000, type: t.number() },
|
|
153
|
+
{ DEBUG: true, type: t.boolean() },
|
|
154
|
+
{ CONFIG: { url: 'https://api.example.com' }, type: t.object() },
|
|
155
|
+
{ ITEMS: [1, 2, 3], type: t.array() },
|
|
156
|
+
]);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Descriptor Format
|
|
160
|
+
|
|
161
|
+
Each descriptor must have:
|
|
162
|
+
- A variable name (key)
|
|
163
|
+
- A value
|
|
164
|
+
- A `type` property with the validator
|
|
165
|
+
|
|
166
|
+
#### Behavior
|
|
167
|
+
|
|
168
|
+
- Throws an error if a descriptor is missing a variable name
|
|
169
|
+
- Throws an error if a descriptor is missing a type
|
|
170
|
+
- Throws an error if a value fails validation for its type
|
|
171
|
+
- Types are automatically inferred from the descriptors
|
|
172
|
+
|
|
173
|
+
## Usage Patterns
|
|
174
|
+
|
|
175
|
+
### Environment Variables
|
|
176
|
+
|
|
177
|
+
Commonly used for validating `.env` files:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { Dictionary } from './lib/dictionary';
|
|
181
|
+
import { t } from './lib/t';
|
|
182
|
+
|
|
183
|
+
const env = new Dictionary({
|
|
184
|
+
env: process.env,
|
|
185
|
+
schema: {
|
|
186
|
+
DATABASE_URL: t.string(),
|
|
187
|
+
PORT: t.number(),
|
|
188
|
+
NODE_ENV: t.string(),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Configuration Object
|
|
194
|
+
|
|
195
|
+
Can be used with any configuration object:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
const config = {
|
|
199
|
+
server: {
|
|
200
|
+
host: 'localhost',
|
|
201
|
+
port: 8080,
|
|
202
|
+
},
|
|
203
|
+
features: {
|
|
204
|
+
auth: true,
|
|
205
|
+
logging: false,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const typedConfig = new Dictionary({
|
|
210
|
+
env: config,
|
|
211
|
+
schema: {
|
|
212
|
+
server: t.object(),
|
|
213
|
+
features: t.object(),
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Nested Configuration
|
|
219
|
+
|
|
220
|
+
For complex nested objects, use the `object` type:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const dbConfig = new Dictionary({
|
|
224
|
+
env: {
|
|
225
|
+
database: {
|
|
226
|
+
host: 'localhost',
|
|
227
|
+
port: 5432,
|
|
228
|
+
ssl: true,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
schema: {
|
|
232
|
+
database: t.object(),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Error Handling
|
|
238
|
+
|
|
239
|
+
Both `Dictionary` and `DictionaryEnv` throw errors when validation fails:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { Dictionary } from './lib/dictionary';
|
|
243
|
+
import { t } from './lib/t';
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const dict = new Dictionary({
|
|
247
|
+
env: {
|
|
248
|
+
PORT: '3000', // Should be a number
|
|
249
|
+
},
|
|
250
|
+
schema: {
|
|
251
|
+
PORT: t.number(),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error(error.message);
|
|
256
|
+
// 'Invalid environment variable "PORT".'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
import { DictionaryEnv } from './lib/dictionary';
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const env = new DictionaryEnv([
|
|
263
|
+
{ PORT: '3000', type: t.number() }, // Should be a number
|
|
264
|
+
]);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(error.message);
|
|
267
|
+
// 'ENV value "PORT" is not valid for the provided type'
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Type Safety
|
|
272
|
+
|
|
273
|
+
Both classes provide full TypeScript type inference:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const env = new DictionaryEnv([
|
|
277
|
+
{ API_KEY: 'secret', type: t.string() },
|
|
278
|
+
{ PORT: 3000, type: t.number() },
|
|
279
|
+
{ DEBUG: true, type: t.boolean() },
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
// TypeScript knows these types:
|
|
283
|
+
env.env.API_KEY; // string
|
|
284
|
+
env.env.PORT; // number
|
|
285
|
+
env.env.DEBUG; // boolean
|
|
286
|
+
env.env.UNKNOWN; // TypeScript error
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
### Install Dependencies
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
bun install
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Build
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
bun run build
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Run Tests
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
bun test
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT
|
|
312
|
+
|
|
313
|
+
## Contributing
|
|
314
|
+
|
|
315
|
+
Contributions are welcome! Please ensure all tests pass before submitting a pull request.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "dictionary",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "latest",
|
|
8
|
+
},
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"typescript": "^5",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"packages": {
|
|
15
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
16
|
+
|
|
17
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
18
|
+
|
|
19
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
20
|
+
|
|
21
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
22
|
+
|
|
23
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
24
|
+
}
|
|
25
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t } from "./lib/t";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: . */
|
|
2
|
+
import type { DictionaryEnvDescriptor, DictionaryOptions, SchemaType } from "./types";
|
|
3
|
+
|
|
4
|
+
export class Dictionary<T extends Record<string, any> = Record<string, any>> {
|
|
5
|
+
public readonly values: T;
|
|
6
|
+
constructor(options: DictionaryOptions) {
|
|
7
|
+
const { env, schema } = options;
|
|
8
|
+
|
|
9
|
+
let typedEnv: any = {};
|
|
10
|
+
if (schema) {
|
|
11
|
+
for (const key in schema) {
|
|
12
|
+
const s = schema[key];
|
|
13
|
+
const value = env[key];
|
|
14
|
+
if (s?.validate(value)) {
|
|
15
|
+
typedEnv[key] = value;
|
|
16
|
+
} else if (s) {
|
|
17
|
+
throw new Error(`Invalid environment variable "${key}".`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
typedEnv = env;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.values = typedEnv;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class DictionaryEnv<
|
|
29
|
+
TSchema extends DictionaryEnvDescriptor<string, any>[],
|
|
30
|
+
> {
|
|
31
|
+
public readonly env: {
|
|
32
|
+
[K in TSchema[number] as K extends { type: SchemaType }
|
|
33
|
+
? keyof K & string
|
|
34
|
+
: never]: K extends { type: infer S }
|
|
35
|
+
? S extends { validate: (input: unknown) => input is infer U }
|
|
36
|
+
? U
|
|
37
|
+
: never
|
|
38
|
+
: never;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
constructor(schema: TSchema) {
|
|
42
|
+
const result: any = {};
|
|
43
|
+
for (const desc of schema) {
|
|
44
|
+
const key = Object.keys(desc).find((k) => k !== "type");
|
|
45
|
+
if (!key) throw new Error("Descriptor must have a variable name");
|
|
46
|
+
const val = (desc as any)[key];
|
|
47
|
+
const type = (desc as any).type;
|
|
48
|
+
if (!type || typeof type.validate !== "function")
|
|
49
|
+
throw new Error("Descriptor must have a type");
|
|
50
|
+
if (!type.validate(val)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`ENV value "${key}" is not valid for the provided type`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
result[key] = val;
|
|
56
|
+
}
|
|
57
|
+
this.env = result;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/lib/t.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: . */
|
|
2
|
+
type SchemaType = "string" | "number" | "boolean" | "object" | "array" | "any";
|
|
3
|
+
|
|
4
|
+
interface Schema<T, S extends SchemaType> {
|
|
5
|
+
type: S;
|
|
6
|
+
validate: (input: unknown) => input is T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const stringSchema: Schema<string, "string"> = {
|
|
10
|
+
type: "string",
|
|
11
|
+
validate: (input): input is string => typeof input === "string",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const numberSchema: Schema<number, "number"> = {
|
|
15
|
+
type: "number",
|
|
16
|
+
validate: (input): input is number => typeof input === "number",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const booleanSchema: Schema<boolean, "boolean"> = {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
validate: (input): input is boolean => typeof input === "boolean",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const objectSchema: Schema<object, "object"> = {
|
|
25
|
+
type: "object",
|
|
26
|
+
validate: (input): input is object =>
|
|
27
|
+
typeof input === "object" && input !== null && !Array.isArray(input),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const arraySchema: Schema<any[], "array"> = {
|
|
31
|
+
type: "array",
|
|
32
|
+
validate: (input): input is any[] => Array.isArray(input),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const anySchema: Schema<any, "any"> = {
|
|
36
|
+
type: "any",
|
|
37
|
+
validate: (_input): _input is any => true,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const t = {
|
|
41
|
+
string: () => stringSchema,
|
|
42
|
+
number: () => numberSchema,
|
|
43
|
+
boolean: () => booleanSchema,
|
|
44
|
+
object: () => objectSchema,
|
|
45
|
+
array: () => arraySchema,
|
|
46
|
+
any: () => anySchema,
|
|
47
|
+
};
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { t } from "./t";
|
|
2
|
+
|
|
3
|
+
type SchemaType = ReturnType<
|
|
4
|
+
| typeof t.string
|
|
5
|
+
| typeof t.number
|
|
6
|
+
| typeof t.boolean
|
|
7
|
+
| typeof t.object
|
|
8
|
+
| typeof t.array
|
|
9
|
+
| typeof t.any
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
type DictionaryOptions = {
|
|
13
|
+
env: Record<string, unknown>;
|
|
14
|
+
schema?: Record<string, SchemaType>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type DictionaryEnvDescriptor<T extends string, V> = { [K in T]: V } & {
|
|
18
|
+
type: SchemaType;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type { SchemaType, DictionaryOptions, DictionaryEnvDescriptor }
|
package/package.json
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
|
|
15
|
+
"strict": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true,
|
|
19
|
+
"noImplicitOverride": true,
|
|
20
|
+
|
|
21
|
+
"noUnusedLocals": false,
|
|
22
|
+
"noUnusedParameters": false,
|
|
23
|
+
"noPropertyAccessFromIndexSignature": false
|
|
24
|
+
}
|
|
25
|
+
}
|