Skip to content

Commit 76127fe

Browse files
feat: add autocomplete and type inference
Adds autocomplete to `config.get` and type inference to the value assigned. For example if you type `config.get('` you'll see a list of suggested values that come directly from whatever was configured by the user in `config.js`. If you've confgiured "port" to be a number in `config.js` and assign a variable like so: `const port = config.get('port')` The variable "port" will be inferred to be a number. Ref: NA
1 parent 8d418d8 commit 76127fe

File tree

4 files changed

+136
-3
lines changed

4 files changed

+136
-3
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,13 @@ const config = require('./config.js')
5454
// This validates that we have the necessary env vars.
5555
config.validateEnvVars()
5656

57+
// When typing `config.get(..)` you should see auto-complete
58+
// for what's been configured in `config.js` and the variable
59+
// assigned should have its type inferred; in this case a `number`.
60+
const port = config.get('port')
61+
5762
http.listen(config.get('port'), () => {
58-
log.info('listen', config.get('port'))
63+
log.info('listen', port)
5964
})
6065
```
6166

index.d.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Type definitions for @logdna/env-config
2+
// Minimal, inferred key/value typing for configuration definitions
3+
4+
// Utility types to extract literal types from arrays
5+
export type ReadonlyStringArray = readonly string[];
6+
7+
// Base definition interface (runtime class is untyped JS; this is a structural representation)
8+
interface BaseDefinition<Name extends string = string, Kind extends string = string> {
9+
_name: Name;
10+
_type: Kind;
11+
// Chainable common methods
12+
required(): this;
13+
desc(str: string): this;
14+
description(str: string): this;
15+
default(val: any): this; // default does not change exposed type here (runtime may still allow null)
16+
allowEmpty(): this;
17+
name(str: string): this;
18+
}
19+
20+
// Enum extension captures allowed values to narrow type
21+
interface EnumCapable<Name extends string = string> extends BaseDefinition<Name, 'enum'> {
22+
values<V extends ReadonlyStringArray>(vals: V): EnumCapableWithValues<Name, V>;
23+
}
24+
interface EnumCapableWithValues<Name extends string, V extends ReadonlyStringArray> extends BaseDefinition<Name, 'enum'> {
25+
readonly __enumValues: V[number];
26+
values(vals: V): this; // further calls keep same type
27+
}
28+
29+
// Regex definition
30+
interface RegexDefinition<Name extends string = string> extends BaseDefinition<Name, 'regex'> {
31+
match(re: string | RegExp): this;
32+
}
33+
34+
// Number definition (min/max chaining)
35+
interface NumberDefinition<Name extends string = string> extends BaseDefinition<Name, 'number'> {
36+
min(n: number): this;
37+
max(n: number): this;
38+
}
39+
40+
// Boolean definition (no special methods beyond base)
41+
interface BooleanDefinition<Name extends string = string> extends BaseDefinition<Name, 'boolean'> {}
42+
43+
// String definition
44+
interface StringDefinition<Name extends string = string> extends BaseDefinition<Name, 'string'> {}
45+
46+
// List definition (captures element type & separator)
47+
interface ListDefinition<Name extends string = string, ElemKind extends ListElementKind | undefined = undefined> extends BaseDefinition<Name, 'list'> {
48+
type<T extends ListElementKind>(t: T): ListDefinitionWithType<Name, T>;
49+
separator(val: string | RegExp): this;
50+
}
51+
interface ListDefinitionWithType<Name extends string, ElemKind extends ListElementKind> extends BaseDefinition<Name, 'list'> {
52+
readonly __listType: ElemKind;
53+
type(t: ElemKind): this; // idempotent if called again
54+
separator(val: string | RegExp): this;
55+
}
56+
57+
type ListElementKind = 'string' | 'number' | 'boolean';
58+
59+
// Aggregate union of any definition forms
60+
export type DefinitionAny =
61+
| StringDefinition<string>
62+
| NumberDefinition<string>
63+
| BooleanDefinition<string>
64+
| RegexDefinition<string>
65+
| EnumCapable<string>
66+
| EnumCapableWithValues<string, ReadonlyStringArray>
67+
| ListDefinition<string>
68+
| ListDefinitionWithType<string, ListElementKind>;
69+
70+
// Infer the names from a readonly tuple of definitions
71+
export type DefinitionNames<Defs extends readonly DefinitionAny[]> = Defs[number]['_name'];
72+
73+
// Find definition by name in tuple
74+
export type FindDefinition<Defs extends readonly DefinitionAny[], N extends string> = Extract<Defs[number], { _name: N }>;
75+
76+
// Map definition kind (and its refinements) to a resulting value type.
77+
// These reflect runtime behavior loosely (null for unset strings/lists, undefined for invalid booleans, etc.)
78+
export type ValueOfDefinition<D> =
79+
D extends EnumCapableWithValues<any, any> ? D['__enumValues'] | null :
80+
D extends { _type: 'enum' } ? string | null :
81+
D extends { _type: 'string' } ? string | null :
82+
D extends { _type: 'number' } ? number :
83+
D extends { _type: 'boolean' } ? boolean | undefined :
84+
D extends { _type: 'regex' } ? string | null :
85+
D extends ListDefinitionWithType<any, infer E> ? (
86+
E extends 'string' ? string[] | null :
87+
E extends 'number' ? number[] | null :
88+
E extends 'boolean' ? (boolean | undefined)[] | null : any
89+
) :
90+
D extends { _type: 'list' } ? any[] | null :
91+
unknown;
92+
93+
// Produce a record type for all definitions in a tuple
94+
export type ConfigShape<Defs extends readonly DefinitionAny[]> = {
95+
[K in DefinitionNames<Defs>]: ValueOfDefinition<FindDefinition<Defs, K>>
96+
};
97+
98+
// Main Env (Config) class declaration
99+
declare class Env<Defs extends readonly DefinitionAny[] = DefinitionAny[]> extends Map<DefinitionNames<Defs>, any> {
100+
constructor(input: Defs);
101+
// Override get/has with key autocomplete and value typing
102+
get<K extends DefinitionNames<Defs>>(key: K): ConfigShape<Defs>[K];
103+
has<K extends DefinitionNames<Defs>>(key: K): boolean;
104+
toJSON(): ConfigShape<Defs>;
105+
validateEnvVars(): void;
106+
// Static builders (preserve name literal type)
107+
static string<N extends string>(name: N): StringDefinition<N>;
108+
static number<N extends string>(name: N): NumberDefinition<N>;
109+
static boolean<N extends string>(name: N): BooleanDefinition<N>;
110+
static regex<N extends string>(name: N): RegexDefinition<N>;
111+
static enum<N extends string>(name: N): EnumCapable<N>;
112+
static list<N extends string>(name: N): ListDefinition<N>;
113+
}
114+
115+
export = Env;

lib/definition.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,20 @@ const VALID_TYPES = new Set([
4141
])
4242

4343
module.exports = class Definition {
44+
/**
45+
* @param {string} type
46+
* @param {T} name
47+
*/
4448
constructor(type, name) {
4549
assert(VALID_TYPES.has(type), `Invalid type: "${type}"`)
50+
if (!name) {
51+
throw new Error('name is required')
52+
}
53+
this._type = type
54+
/** @type {T} */
55+
this._name = name
56+
this._alias = null
57+
this._default = undefined
4658
this._required = false
4759
this._type = type
4860
this._description = null

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "@logdna/env-config",
3-
"version": "2.0.1",
3+
"version": "2.1.0",
44
"description": "Configuration package for reading environment variables",
5-
"main": "index.js",
5+
"types": "index.d.ts",
66
"files": [
77
"bin/**/*",
88
"lib/**/*",
99
"index.js",
10+
"index.d.ts",
1011
"README.md",
1112
"LICENSE"
1213
],

0 commit comments

Comments
 (0)