tyfigo

Schemas

Validate config imports and environment variables with Zod, Valibot, ArkType or Effect Schema.

The content mapper checks types. A schema adds the rules a file can't express (ranges, URLs, enums, required variables) and can transform values. tyfigo accepts any Standard Schema (Zod, Valibot, ArkType, …) and Effect Schema.

import { loadConfig, loadConfigAsync, loadEnv, loadEnvAsync, ConfigValidationError } from 'tyfigo';

loadConfig

loadConfig(config, schema) validates a config import (or any value) and returns the schema's output:

src/main.ts
import { loadConfig } from 'tyfigo';
import { z } from 'zod';

import config from '../config/app.config.yaml';

const Config = z.object({
	name: z.string(),
	server: z.object({ host: z.string(), port: z.number().int().positive() }),
	database: z.object({
		url: z.url({ protocol: /^postgres$/ }),
		pool: z.object({ min: z.number().int(), max: z.number().int() }),
	}),
	features: z.array(z.string()),
});

const settings = loadConfig(config, Config);

The config's type has to fit the schema's input type, so a schema that expects a string where the file has a number is a type error at the call. Keys the schema doesn't list are allowed, and a widened string fits a schema that expects literals such as z.enum([...]). See What tsc checks without a schema for the cases a schema covers.

When validation fails it throws a ConfigValidationError that lists every issue, each with the file:line:column the value came from (the environment file that set it, if one did):

Invalid config /srv/app/config/app.config.yaml:
  /srv/app/config/app.config.yaml:2:9 - server.port: Too small: expected number to be >=1
  /srv/app/config/app.config.yaml:1:1 - server.host: Invalid input: expected string, received undefined

loadEnv

loadEnv(schema, options?) validates environment variables: .env, .env.local, .env.<environment> and .env.<environment>.local, with the real environment on top. It never writes process.env.

import { loadEnv } from 'tyfigo';
import { z } from 'zod';

const env = loadEnv(z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development') }));
OptionDefaultMeaning
cwdprocess.cwd()Where to look for .env files first, before the project root
environmentenv.NODE_ENVPicks .env.<environment> and .env.<environment>.local
enva copy of process.envThe real environment variables, which win over every file

All variables are strings, so use a schema that coerces (z.coerce.number(), v.transform(Number), Schema.FiniteFromString) for numbers.

Async schemas

loadConfig and loadEnv are synchronous. A schema that validates asynchronously (an async refinement, for example) makes them throw a TypeError. Use loadConfigAsync and loadEnvAsync, which return promises.

Libraries

import { loadConfig, loadEnv } from 'tyfigo';
import { z } from 'zod';

const settings = loadConfig(
	config,
	z.object({ server: z.object({ host: z.string(), port: z.number().int().min(1) }), tags: z.array(z.string().min(2)) }),
);
const env = loadEnv(z.object({ PORT: z.coerce.number() }));

ConfigValidationError

PropertyType
_tag'ConfigValidationError'
subject'config /path/app.config.yaml' or 'environment variables'
issuesreadonly { path; message; location? }[]
messageEvery issue, one per line

location is file:line:column for a config import, or the file alone when the key has no position. For loadEnv, a .env file that can't be read is an issue too.

On this page