├── .gitignore
├── .idea
├── .gitignore
├── classenv.iml
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .prettierrc
├── README.md
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── env.ts
└── index.ts
├── test
└── env.spec.ts
├── tsconfig.build.json
├── tsconfig.checks.json
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/classenv.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TypeScript environment variable decorator
2 |
3 | A perfect TypeScript environment variables library.
4 |
5 | - Strongly-typed declarative class containing your environment data
6 | - Supports both static and instance properties
7 | - Type-casting using TypeScript metadata reflection
8 | - Auto UPPER_SNAKE_CASE conversion
9 | - Converts environment values "FALSE", "false", "0" to false for boolean types
10 | - Throws runtime error if variable doesn't exist
11 | - Supports default values
12 | - Makes decorated properties read-only in runtime
13 | - ❤️ You will like it
14 |
15 | ## 💼 Use cases
16 |
17 | ### 🪞 Type-casting Using TypeScript metadata reflection
18 |
19 | Just specify class field type and `classenv` will cast the environment variable string value to the value of your field type.
20 | Only `string`, `number`, and `boolean` is supported.
21 |
22 | ```ts
23 | process.env['PORT'] = '3000';
24 |
25 | class ServerSettings {
26 | @Env('PORT')
27 | portNumber!: number; // 3000
28 | @Env('PORT')
29 | portString!: string; // "3000"
30 | @Env('PORT') // Why not?!
31 | portBoolean!: boolean; // true
32 | }
33 | ```
34 |
35 | ### 🐍 Auto UPPER_SNAKE_CASE from camelCase conversion
36 |
37 | No need to manually specify the environment variable name
38 |
39 | ```ts
40 | process.env['POSTGRES_URL'] = 'postgres://127.0.0.1:5432';
41 |
42 | class PostgresAdapter {
43 | // Field name will be auto-converted to POSTGRES_URL for checking the process.env
44 | @Env()
45 | postgresUrl!: string; // "postgres://127.0.0.1:5432"
46 | }
47 | ```
48 |
49 | ### 🫙 Use default value in case of environment variable absence
50 |
51 | ```ts
52 | class ServerSettings {
53 | @Env()
54 | port: number = 3000; // 3000
55 | }
56 | ```
57 |
58 | ### 🚔 Throw runtime error if no value provided
59 |
60 | One could say `"It's a bad practice to throw runtime error"`, and it's a right assertion, but not in this case.
61 | Most of the time your application can't work without all the environment variables.
62 | You don't want to run application in an indefinite state and then debug these strange things.
63 | So `classenv` will throw runtime error and your application should shut down with an informative message of what's going wrong.
64 |
65 | ```ts
66 | class PostgresAdapter {
67 | @Env()
68 | // Will throw a runtime error, because your app can't work without DB connection
69 | postgresUrl!: string;
70 | }
71 | ```
72 |
73 | But in case the environment variable is not required – you can just assign a default value for the field, and it will not throw.
74 |
75 | ```ts
76 | class PostgresAdapter {
77 | @Env()
78 | postgresUrl: string = 'postgres://127.0.0.1:5432'; // Everything is ok here
79 | }
80 | ```
81 |
82 | ### 🔘 Pick one of the names from array
83 |
84 | ```ts
85 | process.env['POSTGRES_URL'] = 'postgres://127.0.0.1:5432';
86 |
87 | class PostgresAdapter {
88 | @Env(['POSTGRESQL_URI', 'PG_URL', 'POSTGRES_URL'])
89 | url!: string; // "postgres://127.0.0.1:5432"
90 | }
91 | ```
92 |
93 | ### ✨ `static` field also supported
94 |
95 | ```ts
96 | process.env['PORT'] = '3000';
97 |
98 | class ServerSettings {
99 | @Env()
100 | static port: number; // "3000"
101 | }
102 | ```
103 |
104 | ### 1️⃣ Boolean type casting 0️⃣
105 |
106 | If value is `0` of `false` in any case (`FaLsE` also included, since it's `.toLowerCase()`'d under the hood) – it becomes `false`.
107 | Otherwise - `true`
108 |
109 | ```ts
110 | process.env['FALSE'] = 'false';
111 | process.env['ZERO'] = '0';
112 | process.env['TRUE'] = 'true';
113 | process.env['ANYTHING'] = 'Jast a random string';
114 |
115 | class Common {
116 | @Env()
117 | static FALSE!: boolean; // false
118 | @Env()
119 | static zero!: boolean; // false
120 | @Env()
121 | static TRUE!: boolean; // true
122 | @Env()
123 | static anything!: boolean; // true
124 | }
125 | ```
126 |
127 | ### 🛑 `@Env()` decorated properties are read-only in runtime
128 |
129 | Environment is something established from outside, so you definitely should not modify it in your application.
130 |
131 |
132 | ```ts
133 | process.env['PORT'] = '3000';
134 |
135 | class ServerSettings {
136 | @Env()
137 | static port!: number;
138 | }
139 |
140 | // TypeError: Cannot assign to read only property 'port' of function 'class ServerSettings{}'
141 |
142 | ServerSettings.port = 5000;
143 | ```
144 |
145 | ## ❗Dependencies❗
146 |
147 | It is important, `classenv` can not work without it.
148 |
149 | ### reflect-metadata
150 |
151 | ```
152 | npm i reflect-metadata
153 | ```
154 |
155 | And then import it somewhere close to your entry point (`index.ts`/`main.ts`/etc...).
156 | Should be imported before any of your environment classes.
157 |
158 | ```typescript
159 | import 'reflect-metadata';
160 | ```
161 |
162 | ### tsconfig.json
163 |
164 | These settings should be enabled
165 |
166 | ```
167 | "emitDecoratorMetadata": true,
168 | "experimentalDecorators": true,
169 | ```
170 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "classenv",
3 | "version": "1.4.1",
4 | "description": "Describe your environment variables contract with TypeScript class decorator",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "build": "tsc -p tsconfig.build.json",
12 | "test": "npx jest",
13 | "format": "prettier --write \"src/**/*.ts\" --loglevel warn",
14 | "prebuild": "rm -rf ./dist",
15 | "prepublishOnly": "npm run build"
16 | },
17 | "author": "bowzee",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/dilame/classenv"
21 | },
22 | "license": "ISC",
23 | "devDependencies": {
24 | "@types/jest": "^25.1.4",
25 | "@types/node": "^17.0.31",
26 | "jest": "^25.1.0",
27 | "prettier": "^2.7.1",
28 | "ts-jest": "^25.2.1",
29 | "typescript": "^4.8.4"
30 | },
31 | "peerDependencies": {
32 | "reflect-metadata": "^0.1.13"
33 | },
34 | "keywords": [
35 | "typescript",
36 | "class",
37 | "environment",
38 | "env",
39 | "variables",
40 | "decorator",
41 | "dotenv",
42 | ".env",
43 | "config",
44 | "settings",
45 | "process",
46 | "process.env",
47 | "defaults"
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | function upperSnakeCase(str: string): string {
2 | return str
3 | .split(/(?=[A-Z])/)
4 | .join('_')
5 | .toUpperCase();
6 | }
7 |
8 | const FALSEY_VALUES = ['false', '0'];
9 |
10 | export function Env(variableName?: string | string[]): any {
11 | return (target: any, key: string) => {
12 | variableName ??= upperSnakeCase(key);
13 |
14 | let envVarValue: string | undefined = void 0;
15 | if (Array.isArray(variableName)) {
16 | const existingKey = variableName.find(
17 | (k) => typeof process.env[k] === 'string',
18 | );
19 | if (typeof existingKey === 'string') {
20 | envVarValue = process.env[existingKey];
21 | }
22 | } else {
23 | envVarValue = process.env[variableName];
24 | }
25 |
26 | let outValue: string;
27 | if (typeof envVarValue === 'undefined') {
28 | if (typeof target[key] === 'undefined') {
29 | throw new Error(`CLASSENV: Environment variable ${JSON.stringify(variableName)} is undefined and default value for field ${JSON.stringify(key)} not set`);
30 | } else {
31 | outValue = target[key];
32 | }
33 | } else {
34 | outValue = envVarValue;
35 | }
36 | const DesignType = (Reflect as any).getMetadata('design:type', target, key);
37 | if ([String, Number, Boolean].includes(DesignType)) {
38 | if (
39 | DesignType === Boolean &&
40 | FALSEY_VALUES.includes(outValue.toLowerCase())
41 | ) {
42 | outValue = '';
43 | }
44 | outValue = DesignType(outValue);
45 | Object.defineProperty(target, key, {
46 | value: outValue,
47 | writable: false,
48 | enumerable: true,
49 | configurable: true,
50 | });
51 | } else {
52 | throw new Error(
53 | `CLASSENV: ${key} type must be one of [String, Number, Boolean]. Got ${DesignType.name}`,
54 | );
55 | }
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './env';
2 |
--------------------------------------------------------------------------------
/test/env.spec.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import { Env } from '../src';
3 |
4 | describe('env decorator', () => {
5 | process.env['TEST_TEST'] = '1';
6 | process.env['BOOLEAN_FALSE_STRING'] = 'false';
7 | process.env['BOOLEAN_ZERO_STRING'] = '0';
8 |
9 | class Environment {
10 | @Env('TEST_TEST')
11 | static testStr: string;
12 | @Env('TEST_TEST')
13 | static testNmbr: number;
14 | @Env('TEST_TEST')
15 | static testBln: boolean;
16 | @Env()
17 | static testTest: number;
18 | @Env()
19 | static withDefault: string = 'yeah its me';
20 | @Env()
21 | static booleanFalseString: boolean;
22 | @Env()
23 | static booleanZeroString: boolean;
24 | @Env()
25 | testTest!: number;
26 | }
27 |
28 | it('should throw when variable not exists', () => {
29 | expect(() => Env('not-existing')()).toThrow();
30 | });
31 |
32 | it('should cast types', () => {
33 | expect(typeof Environment.testStr).toBe('string');
34 | expect(typeof Environment.testNmbr).toBe('number');
35 | expect(typeof Environment.testBln).toBe('boolean');
36 | expect(typeof Environment.testTest).toBe('number');
37 | });
38 |
39 | it('should auto convert to upper-snake-case', () => {
40 | expect(typeof Environment.testTest).toBe('number');
41 | });
42 |
43 | it('should not throw when default value is set', () => {
44 | expect(Environment.withDefault).toBe('yeah its me');
45 | });
46 |
47 | it('should throw when trying to mutate', () => {
48 | expect(() => (Environment.testStr = '1')).toThrow();
49 | });
50 |
51 | it('should support instance properties', () => {
52 | const env = new Environment();
53 | expect(typeof env.testTest).toBe('number');
54 | });
55 |
56 | it('should cast strings "false", "FALSE" and "0" to boolean for boolean types', () => {
57 | expect(Environment.booleanFalseString).toStrictEqual(false);
58 | expect(Environment.booleanZeroString).toStrictEqual(false);
59 | });
60 |
61 | describe('array variable name', () => {
62 | it('should take first existing environment variable in a right order', () => {
63 | class Environment {
64 | @Env(['NON-EXISTING', 'TEST_TEST', 'BOOLEAN_FALSE_STRING'])
65 | test!: string;
66 | }
67 | const env = new Environment;
68 | expect(env.test).toBe('1');
69 |
70 | });
71 | });
72 | });
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "test"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.checks.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Strict Checks
4 | "alwaysStrict": true,
5 | "noImplicitAny": true,
6 | "strictNullChecks": true,
7 | "useUnknownInCatchVariables": true,
8 | "strictPropertyInitialization": true,
9 | "strictFunctionTypes": true,
10 | "noImplicitThis": true,
11 | "strictBindCallApply": true,
12 | "noPropertyAccessFromIndexSignature": true,
13 | "noUncheckedIndexedAccess": true,
14 | // Linter Checks
15 | "noImplicitReturns": true,
16 | // https://eslint.org/docs/rules/consistent-return ?
17 | "noFallthroughCasesInSwitch": true,
18 | // https://eslint.org/docs/rules/no-fallthrough
19 | "noUnusedLocals": true,
20 | // https://eslint.org/docs/rules/no-unused-vars
21 | "noUnusedParameters": true,
22 | // https://eslint.org/docs/rules/no-unused-vars#args
23 | "allowUnreachableCode": false,
24 | // https://eslint.org/docs/rules/no-unreachable ?
25 | "allowUnusedLabels": false,
26 | // https://eslint.org/docs/rules/no-unused-labels
27 | // Base Strict Checks
28 | "noImplicitUseStrict": false,
29 | "suppressExcessPropertyErrors": false,
30 | "suppressImplicitAnyIndexErrors": false,
31 | "noStrictGenericChecks": false
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.checks.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "declaration": true,
6 | "declarationMap": true,
7 | "removeComments": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es2017",
11 | "sourceMap": true,
12 | "outDir": "./dist",
13 | "baseUrl": "./",
14 | }
15 | }
16 |
--------------------------------------------------------------------------------