Skip to content

Commit

Permalink
fix: use TypeScript overloading (#99)
Browse files Browse the repository at this point in the history
TS v5 introduced `@overload` in JavaScript, so we can now do this.

In addition, created an (optional--non-breaking) generic `T` which is the `source` prop of `Env`, which provides a narrower type if desired.

- Fixed a couple problems around incompatible use of `PropertyKey`
- Added `@types/node` for `process.env` type
- Added `@tsconfig/node16` to use a reasonable configuration targeting Node.js v16.x
  • Loading branch information
boneskull committed Oct 31, 2023
1 parent 041b6a7 commit 6592f06
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 34 deletions.
48 changes: 40 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
"license": "BSD-3-Clause",
"devDependencies": {
"@eslint/js": "^8.49.0",
"@tsconfig/node16": "^16.1.1",
"@types/node": "20.8.9",
"chai": "4.3.10",
"eslint": "^8.21.0",
"lint-staged": "14.0.1",
Expand Down
76 changes: 53 additions & 23 deletions src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,37 +92,51 @@ export class EnvEmptyStringError extends Error {

/**
* A utility for interacting with environment variables
*
* @template {Record<string, string>} [T=Record<string, string>]
*/
export class Env {

/**
* Creates a new instance of Env.
* @param {object} [source] The environment variable object to read from.
* @param {T} [source] The environment variable object to read from.
*/
constructor(source = defaultEnvSource) {

/**
* The object from which to read environment information.
* @type {object}
* @type {T}
*/
this.source = source;
}

/**
* Retrieves an environment variable without checking for its presence.
* Optionally returns a default value if the environment variable doesn't
*
* Returns a default value if the environment variable doesn't
* exist.
* @template [S=undefined]
*
* @overload
* @param {string} key The environment variable name to retrieve.
* @param {S} [defaultValue] The default value to return if the
* @param {string} defaultValue The default value to return if the
* environment variable is not found.
* @returns {S extends string ? S : (string|S)} The environment variable value if found or undefined if not.
* @returns {string} The environment variable value if found or undefined if not.
*/
get(key, defaultValue) {
/** @type {S extends string ? S : undefined} */
/**
* Retrieves an environment variable without checking for its presence.
*
* @overload
* @param {string} key
* @returns {string|undefined}
*/
/**
* @param {string} key
* @param {string} [defaultValue]
*/
get(key, defaultValue) {
let value = undefined;
if (typeof defaultValue !== "undefined") {
value = /** @type {S extends string ? S : undefined} */(String(defaultValue));
value = String(defaultValue);
}

return (key in this.source) ? this.source[key] : value;
Expand All @@ -138,18 +152,34 @@ export class Env {
return key in this.source;
}


/**
* Retrieves the first environment variable found in a list of environment
* variable names.
* Optionally returns a default value if the environment variable doesn't
* variable names.
*
* Returns a default value if the environment variable doesn't
* exist.
* @template [S=undefined]
* @param {string[]} keys An array of environment variable names.
* @param {S} [defaultValue] The default value to return if the
*
* @overload
* @param {string[]} keys The environment variable name to retrieve.
* @param {string} defaultValue The default value to return if the
* environment variable is not found.
* @returns {S extends string ? S : (string|S)} The environment variable value if found or undefined if not.
* @returns {string} The environment variable value if found or undefined if not.
* @throws {TypeError} If keys is not an array with at least one item.
*/
/**
* Retrieves the first environment variable found in a list of environment
* variable names.
*
* @overload
* @param {string[]} keys
* @returns {string|undefined}
* @throws {TypeError} If keys is not an array with at least one item.
*/
/**
* @param {string[]} keys
* @param {string} [defaultValue]
*/
first(keys, defaultValue) {

if (!Array.isArray(keys) || keys.length < 1) {
Expand All @@ -161,10 +191,10 @@ export class Env {
return this.source[key];
}
}
/** @type {S extends string ? S : undefined} */
/** @type {string|undefined} */
let value = undefined;
if (typeof defaultValue !== "undefined") {
value = /** @type {S extends string ? S : undefined} */(String(defaultValue));
value = String(defaultValue);
}

return value;
Expand Down Expand Up @@ -214,12 +244,13 @@ export class Env {
* Lazy-loading property containing a proxy that can be used to
* automatically throw errors when an undefined environment variable
* is accessed.
* @returns {object} A proxy object.
* @returns {T} A proxy object.
*/
get exists() {

const existsProxy = new Proxy(this.source, {
get(target, key) {
key = String(key);
if (key in target) {
return target[key];
}
Expand All @@ -236,20 +267,20 @@ export class Env {
configurable: false
});

/** @type {object} */
return existsProxy;
}

/**
* Lazy-loading property containing a proxy that can be used to
* automatically throw errors when an undefined or empty string
* environment variable is accessed.
* @returns {object} A proxy object.
* @returns {T} A proxy object.
*/
get required() {

const requiredProxy = new Proxy(this.source, {
get(target, key) {
key = String(key);
if (key in target) {
if (target[key] === "") {
throw new Env.EmptyStringError(key);
Expand All @@ -270,20 +301,19 @@ export class Env {
configurable: false
});

/** @type {object} */
return requiredProxy;
}

}

/**
* The error to use when a key isn't found.
* @type {new (key: PropertyKey) => Error}
* @type {new (key: string) => Error}
*/
Env.KeyNotFoundError = EnvKeyNotFoundError;

/**
* The error to use when a key isn't found.
* @type {new (key: PropertyKey) => Error}
* @type {new (key: string) => Error}
*/
Env.EmptyStringError = EnvEmptyStringError;
7 changes: 4 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"include": [
"extends": "@tsconfig/node16/tsconfig.json",
"files": [
"src/env.js"
],
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"allowJs": true,
"checkJs": true,
"target": "ES2015",
"outDir": "dist"
"outDir": "dist",
"types": ["node"]
}
}

0 comments on commit 6592f06

Please sign in to comment.