npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@emartech/octopus-config-ts

v1.0.1

Published

Schema-validated environment variable and config loading for TypeScript and JavaScript, with type inference.

Maintainers

mruellmruellakapaakapakaajkaajknagyknagyjfillmorejfillmorenorbert-levajsics-emarsysnorbert-levajsics-emarsysmmartinmmartinbsoosbsoosemarsys-deployeremarsys-deployerdrahos.istvandrahos.istvanjudgejudgeemarsys-securityemarsys-securitydaniel.bankydaniel.bankyszeistszeistmarton.papp.emarsysmarton.papp.emarsysdgyenesdgyeness.viktors.viktorm4w4q7m4w4q7david.barkoczidavid.barkoczidaniels1404daniels1404xin.hexin.heqw3rqw3rtamas.tothtamas.tothgergaczdgergaczdviauviaualkraalkraepgrubmairepgrubmairbence.utobence.utoettancosettancosepmartiniepmartinigabor.balla.emarsysgabor.balla.emarsysmzsombormzsomborejperssonejperssonllosonczyllosonczyiben12iben12kartonfarkaskartonfarkasadamoaadamoapevapevabforgacsbforgacszsomborhzsomborhkozmakozmangabor84ngabor84andrasp3aandrasp3aedosreckiedosreckifhasefhasekkimakkkimakgaborbgaborbglendvaiglendvailverasztolverasztobozsadambozsadamrdoczirdoczifentosifentosiboristomicboristomicmbazsombazsodmihalekdmihaleklhalaszlhalaszjason-nelson-01jason-nelson-01dsztankodsztankotbugartbugarfqqdkfqqdkcsabafarago-emarsyscsabafarago-emarsyssarakollsarakollmmolnar-emarmmolnar-emarattila.galattila.galbenjamingehlbenjamingehlrkumari03rkumari03tdorkaatdorkaalkonyalkonyalloki-emarsyslloki-emarsysmrmeszarosmrmeszarosvarszegikvarszegiki850773i850773dfaragodfaragoestefanlesnjakovicestefanlesnjakovicmhunyadymhunyadyatittelatittelmarton.matusekmarton.matuseksixstepsixstepsaphendricksjoergsaphendricksjoergmmothersillmmothersilljerryrichardsonjerryrichardsonarnaud.buchholzarnaud.buchholzjamescockerjamescockeradamszabolcsadamszabolcsmengjiao.zhaomengjiao.zhaoianhelmrichianhelmrichmmartin2mmartin2dkorposdkorposjbleclercjbleclercnvkaur2nvkaur2tbucsanszkitbucsanszkipendicg24pendicg24pinterapinteranicolaeciumacnicolaeciumacdimitrovndimitrovnmaurogrecomaurogrecobercziandbercziandnish343nish343scotthetrickscotthetrickkonradschewekonradscheweivanfroehlichivanfroehlichzbalazszbalazsbiamateibiamateioemlermoemlermmattfeldhake_emarsysmattfeldhake_emarsysbborsibborsisovagossovagosapoonapoonburciburcimark.adorjanmark.adorjanxueboliangxueboliangskrivooskrivookoloshkoloshjviesersapjviesersapdrewhodsonsapdrewhodsonsapcrileycrileyiulianmihaiiulianmihaimariannagmariannagbobby_russelbobby_russeldkocsis-emarsysdkocsis-emarsysmarko.fritzschemarko.fritzschedemajo_emsdemajo_emstothmarci25tothmarci25tsiraitnpmtsiraitnpmplsapplsaproxanamsroxanamsbencekadaremarbencekadaremarviktor.szellviktor.szellccarrollemccarrollemgeczirobertgeczirobertsap-nssap-nsdhruv-sapdhruv-sapirvansapirvansapmarkjarvismarkjarvisrollandghrollandghtillmannrtillmannreggarciaeggarciai517674i517674rimo86rimo86arielkovacssaparielkovacssapsevket-atasevensevket-atasevenzhollerzhollerrobertoraczemarsysrobertoraczemarsyssamthomas02samthomas02mfel0123mfel0123luca.fasolino.seluca.fasolino.sendomkendomkefranziskajungfranziskajungandras.sarroandras.sarrodschuppadschuppaemarsys-stephen-ivesemarsys-stephen-ivesbencsobencsorcsullagrcsullagdputtadputtadwolter_emarsysdwolter_emarsysigordudassapigordudassapahorvath1991ahorvath1991dr.jdr.jjisaacsonjisaacsontothbence8tothbence8d056437d056437ekkovatsekkovatslhammerllhammerliabrahamiabrahamandras-marton-emarsysandras-marton-emarsyssfarielsfarielschroedersteffenschroedersteffendanielisapdanielisaptkatona-saptkatona-sapboldizsarnagyboldizsarnagyllakatosllakatosgillyesgillyestade82tade82dpkemarsysdpkemarsyskarlabrandlkarlabrandltonyhsaptonyhsapaidanlesh-sapaidanlesh-saplucad2007lucad2007nikolett.tarnikolett.tarsridevirsridevirabieler-sapabieler-sapi534456i534456daniel.balazsdaniel.balazslaszlo.orilaszlo.oritroywiegandtroywiegandnoahdojannoahdojanbcsizmadiabcsizmadiaariceemariceemcsjakaboscsjakabosborcsaborcsannieman-sapnnieman-sapsap-amsap-amvszegedivszegedironnykrosseronnykrossetaylor-sethtaylor-sethmrclsumrclsufcolombo-sapfcolombo-sapmfawalmfawalszbszbaleksandrasapaleksandrasapmathischallandmathischallandljanosi-sapljanosi-sapgschneider-sapgschneider-sapbrandon-sapbrandon-sapatittelsapatittelsapbarna.barakbarna.barakpjohnson02pjohnson02dominiknyergesdominiknyergesbence.tothbence.tothweselyn1995weselyn1995azorahai3904azorahai3904levente.otti.saplevente.otti.saptorokhelentorokhelenejwalkerejwalkermatusekmamatusekmalvntelvntejeno.paljeno.paleadanieleadanielppatel-emarsysppatel-emarsysasif.sapasif.sapcsmatyaspeticsmatyaspetikialvatlankialvatlanobijoel-sapobijoel-sapsven.strippsven.strippmnguetsamnguetsalevente_szremkolevente_szremkoferenckalvinsapferenckalvinsapaymenbidasapaymenbidasapbanattibanattittoth2ttoth2vicabartavicabartasap_hangsap_hangkharitonoffkharitonoffmwires_sapmwires_sapazsoldiazsoldiklonigantonklonigantonarien.de.haanarien.de.haanermafteiuscaiermafteiuscaikapegakapegaivantorokivantorokgyorgyabelgyorgyabelbence.nagy.emarsysbence.nagy.emarsysasciortinotwoasciortinotwooliverweisenburgeroliverweisenburgersapcardsapcarderikghererikgherktakacsktakacsdrilonjasharisapdrilonjasharisappeterforis-emarsyspeterforis-emarsysnielsvbijsterveldt-emarsysnielsvbijsterveldt-emarsyszsomborkincseszsomborkincseseergarteergartmfearsmfearsfinally-fancyfinally-fancymagdafabianmagdafabianyunyesapyunyesaparamovicaramovicronnie_sapronnie_sapivan.torok-eivan.torok-efulopgaborfulopgaborkovacs.zsolti.zsoltkovacs.zsolti.zsoltszilvia.barnakszilvia.barnaksachinmahajansachinmahajanadurraadurraszilviabarnakszilviabarnakmfawallmfawallmknuramknurajuliaserrjuliaserrpeterlukacspeterlukacsagruczaagruczagavinsun-sapgavinsun-sapjuliandietrichjuliandietrichnlazasapnlazasapjohnnybakuczjohnnybakuczbence-illia-emarsysbence-illia-emarsysmiklostorbicsmiklostorbicsznemesznemesrayhanwkapadiarayhanwkapadiafarkasandrasfarkasandrasbenedek.izso.emarsysbenedek.izso.emarsysbalint-barki-sapbalint-barki-sapjohannes_thiede_2001johannes_thiede_20010505hendrk0505hendrkflorianmaakflorianmaakeszter.kotraeszter.kotrahewaymanhewaymanruchidhiman-sapruchidhiman-sap

Readme

@emartech/octopus-config-ts

Schema-validated environment variable and config loading for TypeScript and JavaScript, with type inference.

  • Type-safe: the returned config is inferred from your schemas.
  • Aggregating: every invalid variable is collected and reported in one error, not one at a time.

Libraries like envalid or env-var validate the environment and hand back the parsed values. @emartech/octopus-config-ts also lets you derive the config itself — nested shape, defaults, derived values, and conditional variables — in one place, so what you get back is the config, not a typed view over process.env.

Basic usage

Describe your config inside loadConfig and each variable is parsed against a schema:

import { loadConfig, s } from "@emartech/octopus-config-ts";

const config = loadConfig((env) => ({
  nodeEnv: env("NODE_ENV", s.enumOf(["production", "test", "dev"])),
  apiKey: env("API_KEY", s.string()),
  limit: env("LIMIT", s.integer().min(10).max(100).default(25)),
  database: {
    url: env("DB_URL", s.string().url()),
    queryTimeout: env("DB_QUERY_TIMEOUT", s.integer().min(0).default(30_000)),
  },
  featureFlag: env("FEATURE_FLAG", s.list(s.string()).default([])),
}));

/*
const config: {
    nodeEnv: "production" | "test" | "dev";
    apiKey: string;
    limit: number;
    database: {
        url: string;
        queryTimeout: number;
    };
    featureFlag: string[];
}
*/

If anything fails to parse, loadConfig throws a single EnvError listing every problem:

3 invalid variable(s):
  - NODE_ENV   must be one of: production, test, dev
  - LIMIT      must be <= 100, got 500
  - DB_URL     is required but is not set

defineConfig (optional)

For larger apps you may want the config definition to live in its own module. defineConfig is an identity helper that preserves the callback's type so loadConfig can still infer the config shape:

// config.ts
import { defineConfig, s } from "@emartech/octopus-config-ts";

export const define = defineConfig((env) => ({
  nodeEnv: env("NODE_ENV", s.enumOf(["production", "test", "dev"])),
  apiKey: env("API_KEY", s.string()),
  //...
}));

// main.ts
import { loadConfig } from "@emartech/octopus-config-ts";
import { define } from "./config";

const config = loadConfig(define);

Because defineConfig returns the callback itself, a definition can be called with env from inside another one, so config modules compose:

import { defineConfig, s } from "@emartech/octopus-config-ts";

const databaseConfig = defineConfig((env) => ({
  url: env("DB_URL", s.string().url(["postgres"])),
  queryTimeout: env("DB_QUERY_TIMEOUT", s.integer().min(0).default(30_000)),
  //...
}));

const mainConfig = defineConfig((env) => ({
  nodeEnv: env("NODE_ENV", s.enumOf(["production", "test", "dev"])),
  db: databaseConfig(env),
  //...
}));

const config = loadConfig(mainConfig);

You never need defineConfig when a single definition is inline. It exists to keep the callback typed when it's declared apart from its caller, and to let definitions compose.

Reading from a custom source

loadConfig reads from process.env by default. Pass a second argument to read from anywhere, useful in tests:

const ENV = {
  NODE_ENV: "test",
  API_KEY: "secret-key",
  //...
};

loadConfig(define, ENV);

loadConfig reads process.env, it does not populate it. Getting variables into process.env is the caller's job: this library does not read .env files or load dotenv. If you use a .env file, load it (e.g. import "dotenv/config") before calling loadConfig.

Schemas

The s namespace bundles all schema constructors. You can also import them individually (string, integer, float, boolean, enumOf, list).

Types

s.string(); // string
s.integer(); // number (integer)
s.float(); // number
s.boolean(); // boolean
s.enumOf(["a", "b", "c"]); // "a" | "b" | "c"
s.list(s.integer()); // number[]

Refiners

Every schema can be chained with refiners. Refiners narrow the accepted values: they run after parsing succeeded, and reject values that would otherwise be well-typed.

Available refiners by type:

| Schema | Refiners | | ------------------ | --------------------------------------------------------------------- | | all | .refine(predicate, message) | | string | .minCharacters, .maxCharacters, .characters, .matches, .url | | integer, float | .min, .max | | integer | .minDigits, .maxDigits, .digits, .port | | list | .minEntries, .maxEntries, .entries, .unique |

They compose freely:

s.string().minCharacters(1).maxCharacters(64);
s.integer().min(0).max(100);
s.integer().port();
s.string().url();
s.string().matches(/^[a-z0-9-]+$/);
s.list(s.string()).minEntries(1).unique();

s.string().refine((v) => v.startsWith("sk-"), "must start with sk-");

Terminals

By default a variable is required. .optional() and .default(...) offer different ways to handle an unset variable. They change the schema's output type, and don't return a chainable schema.

.optional() widens the output to T | undefined — an unset variable produces undefined instead of a "required" error. When the variable is set, parsing and refiners run as usual:

s.string().optional(); // string | undefined

.default(value) substitutes a fallback when the variable is unset, so the output stays T. value is the parsed type, not a raw string:

s.integer().min(0).default(3000); // number, always defined

.default(value) runs the refiners against the fallback when the schema is constructed and throws if the fallback wouldn't pass, so you cannot ship a default that violates its own schema.

Parser options

Each schema constructor accepts an options object that controls how the raw input is read. Schemas that take an argument (s.enumOf, s.list) accept it as the second parameter:

s.integer({ strict: false });
s.enumOf(["dev", "prod"], { caseSensitive: false });
s.list(s.string(), { separator: ";" });

Shared by all schemas:

  • trimInput (default true): strip whitespace around the raw input.
  • emptyAsUndefined (default true): treat an empty (post-trim) value as unset, so it routes through .optional() / .default() / the required-error path.

s.integer(), s.float():

  • strict (default true): require a plain decimal literal. When false, falls back to Number(raw) (accepts "0x10", "1e5", "+5", etc.). Values must still be finite / integers.

s.boolean():

  • caseSensitive (default false).
  • truthy / falsy (defaults ["1", "true", "yes", "on"] and ["0", "false", "no", "off"]): the values parsed as true and false. Each replaces its default set rather than extending it.

s.enumOf():

  • caseSensitive (default true): when false, matches case-insensitively and returns the canonical casing from the values list.

s.list():

  • separator (default ",").
  • trimEntries (default true): trim each entry after splitting.
  • filterEmptyEntries (default true): drop empty entries (post-trim).

Advanced usage

Why values are Env<T> inside define

Inside the define callback, env(...) does not return the parsed value directly — it returns an Env<T> wrapper:

loadConfig((env) => {
  const nodeEnv = env("NODE_ENV", s.enumOf(["production", "test", "dev"]));
  // Env<"production" | "test" | "dev">, not the union itself

  return { nodeEnv };
});

The callback runs before any variable is known to be readable. NODE_ENV may be missing, or present but unparseable, so at the moment env("NODE_ENV", ...) returns there may be no value to hand back. Env<T> stands in for that value: it lets you describe what the config should do with the variable without requiring the variable to exist yet.

This is what makes error collection possible. A failed read produces a failed Env, and derivations over it are skipped rather than executed, so the callback always runs to completion — every variable is read, and loadConfig reports every failure at once instead of stopping at the first.

The Env wrapper never reaches your application. The tree returned from define may contain Env<T> values nested in objects and arrays; loadConfig unwraps them, stripping the wrapper from the return type.

To derive something from a variable, use one of the combinators below.

.map, .match, .chain

.map(fn)          // Env<T> → Env<U>, transforms the value
.match(cases, ?fallback)  // Env<K> → Env<U>, switch on a literal value
.chain(fn)        // Env<T> → Env<U>, where fn returns another Env

.map for plain transformations:

const nodeEnv = env("NODE_ENV", s.enumOf(["production", "test", "dev"]));
const cookieSecure = nodeEnv.map((v) => v === "production");

Note that cookieSecure derives from the existing nodeEnv binding rather than reading NODE_ENV again. Each name may only be read once inside the callback — a second read throws an EnvUsageError, which also rules out parsing the same variable under two different schemas.

// ❌ NODE_ENV is read twice (and with different schemas)
loadConfig((env) => ({
  nodeEnv: env("NODE_ENV", s.enumOf(["production", "test", "dev"])),
  cookieSecure: env("NODE_ENV", s.string()).map((v) => v === "production"),
}));

// ✅ bind once, derive from the binding
loadConfig((env) => {
  const nodeEnv = env("NODE_ENV", s.enumOf(["production", "test", "dev"]));

  return {
    nodeEnv,
    cookieSecure: nodeEnv.map((v) => v === "production"),
  };
});

The restriction is on the read, not on the value — a single binding can feed any number of derivations.

Don't return an Env from .map — it throws an EnvUsageError. Use .match or .chain for that.

.match

For branching on a value. .match is exhaustive, TypeScript requires a case for every member or a fallback as the second argument. With s.string() the set of values is open, so a fallback is always required.

Cases may return plain values or Env values, and only the selected case runs.

const storage = env("STORAGE", s.enumOf(["s3", "disk"])).match({
  s3: () => env("S3_BUCKET", s.string()),
  disk: () => env("DISK_PATH", s.string()),
});

.chain

The general form of .match. So anything .match expresses, .chain can too, just more verbosely.

Reach for .chain only when the branch isn't a string dispatch: a boolean, a predicate, a comparison, or a discriminant nested inside an object. The callback must return an Env, so plain-value branches need env.const.

const dbUrl = env("USE_TEST_DB", s.boolean()).chain((useTest) =>
  useTest ? env("DB_URL_TEST", s.string()) : env("DB_URL", s.string()),
);

env.group

Read several variables that share a schema into a labelled object. Useful for feature toggles keyed by customer, where each variable holds the same shape (a list of customer IDs) and only the name changes:

const customerIdList = s.list(s.integer()).default([]);

const config = loadConfig((env) => ({
  betaFeatures: env.group(customerIdList, {
    newDashboard: "BETA_NEW_DASHBOARD_CUSTOMERS",
    fastCheckout: "BETA_FAST_CHECKOUT_CUSTOMERS",
    aiSuggestions: "BETA_AI_SUGGESTIONS_CUSTOMERS",
  }),
}));

// config.betaFeatures: {
//   newDashboard: number[],
//   fastCheckout: number[],
//   aiSuggestions: number[],
// }

Defining the schema on its own line keeps the group readable and lets you reuse it. Every value in the group must share the schema, so pulling it out makes that explicit.

env.prefixed

Read a block of variables that share a common name prefix. Inside the callback, env behaves normally except every name is prefixed:

const config = loadConfig((env) => ({
  db: env.prefixed("DB_", (env) => ({
    host: env("HOST", s.string()), // reads DB_HOST
    port: env("PORT", s.integer()), // reads DB_PORT
    name: env("NAME", s.string()), // reads DB_NAME
  })),
}));

Prefixes nest: a prefixed inside a prefixed concatenates:

env.prefixed("DB_", (env) =>
  env.prefixed("PRIMARY_", (env) => ({
    host: env("HOST", s.string()), // reads DB_PRIMARY_HOST
  })),
);

env.const

Lift a plain value into an Env.

const lifted = env.const(123); // Env<number>

env.all and env.struct

Combine several Env values into one. env.all into a tuple, env.struct into an object:

const config = loadConfig((env) => ({
  db: {
    url: env
      .all(
        env("DB_HOST", s.string()),
        env("DB_PORT", s.integer().port()),
        env("DB_NAME", s.string()),
      )
      .map(([host, port, name]) => `postgres://${host}:${port}/${name}`),
  },
}));

Use env.all when the inputs collapse into a single value, and env.struct when you want to keep them as named fields:

const redis = env.struct({
  host: env("REDIS_HOST", s.string()),
  port: env("REDIS_PORT", s.integer().port()),
});
// Env<{ host: string; port: number }>

Either way, every input is read even if an earlier one failed, so all failures are reported together — and the result fails as a unit, so the group is present in full or not at all.

Reusable config blocks

When the same shape appears more than once under different prefixes, combine defineConfig with env.prefixed to define the block once and stamp it out per instance.

A defineConfig callback takes env and returns config, which is exactly what env.prefixed expects — so you can pass one straight into the other:

const batchOptions = defineConfig((env) => ({
  prefetchCount: env("PREFETCH_COUNT", s.integer().default(2048)),
  batchSize: env("BATCH_SIZE", s.integer().default(1000)),
  batchTimeout: env("BATCH_TIMEOUT", s.integer().default(30000)),
  retryTime: env("RETRY_TIME", s.integer().default(5000)),
}));

const config = loadConfig((env) => ({
  refundsBatchOptions: env.prefixed("REFUNDS_", batchOptions),
  flexibleRefundsBatchOptions: env.prefixed("FLEXIBLE_REFUNDS_", batchOptions),
}));

Each env.prefixed(..., batchOptions) reads REFUNDS_PREFETCH_COUNT, FLEXIBLE_REFUNDS_PREFETCH_COUNT, and so on. The block is defined once; only the prefix changes.

When instances need different defaults, wrap the definition in a plain function that takes overrides:

type BatchDefaults = Partial<{
  prefetchCount: number;
  batchSize: number;
  batchTimeout: number;
  retryTime: number;
}>;

const batchOptions = (defaults: BatchDefaults = {}) =>
  defineConfig((env) => ({
    prefetchCount: env(
      "PREFETCH_COUNT",
      s.integer().default(defaults.prefetchCount ?? 2048),
    ),
    batchSize: env(
      "BATCH_SIZE",
      s.integer().default(defaults.batchSize ?? 1000),
    ),
    batchTimeout: env(
      "BATCH_TIMEOUT",
      s.integer().default(defaults.batchTimeout ?? 30000),
    ),
    retryTime: env(
      "RETRY_TIME",
      s.integer().default(defaults.retryTime ?? 5000),
    ),
  }));

const config = loadConfig((env) => ({
  refundsBatchOptions: env.prefixed("REFUNDS_", batchOptions()),
  flexibleRefundsBatchOptions: env.prefixed(
    "FLEXIBLE_REFUNDS_",
    batchOptions(),
  ),
  contactChangesBatchOptions: env.prefixed(
    "CONTACT_CHANGES_",
    batchOptions({ batchSize: 250 }),
  ),
}));

Errors

loadConfig() throws two error classes:

EnvError — thrown by loadConfig when one or more variables failed to parse.

EnvUsageError — thrown when the library is misused, not when input is bad. Signals a bug in the calling code, not a config problem. Cases:

  • Reading the same variable twice inside define.
  • Calling env(...) after loadConfig has returned.
  • Returning an Env from a .map callback (use .chain).
  • Calling .match without a case for the value and without a fallback.