
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
A functional programming and utility library for TypeScript that provides tools for explicit error handling, runtime validation, and asynchronous operations.
chas is a functional programming and utility library for TypeScript. It provides an ergonomic set of tools and monads for explicitly handling errors, defining runtime types, schema validation, managing asynchronous operations, and composing functions.
View the full API documentation here!
Unlike other utility libraries with disparate modules, chas focuses heavily on creating a cohesive and tightly-integrated ecosystem. Its modules are built to work well together, giving you type-safety, intelligent errors, and clean APIs.
At a high level, the chas ecosystem provides:
try/catch or null checks.ts-chas/result)Inspired by Rust and fp-ts, Result<T, E> and ResultAsync<T, E> replace implicit try...catch blocks with explicit, monadic error handling. ResultAsync is also PromiseLike, so it can be awaited or .then()'d directly.
import { chas } from 'ts-chas';
// Predictable error typing
function fetchUser(id: number): chas.ResultAsync<User, string> {
return chas.fromPromise(
fetch(`/users/${id}`).then(res => res.json()),
() => 'Failed to fetch user'
);
}
// Ergonomic chaining and transformation
const finalResult = await fetchUser(1)
.map(user => user.name)
.mapErr(err => err.toUpperCase())
.tap(user => console.log(`Hello, ${user}`)) // side effect on Ok
.tapErr(err => console.log(`Error: ${err}`)) // side effect on Err
.orElse(err => chas.ok(`Using default value: ${err}`));
if (finalResult.isOk()) {
console.log(`Hello, ${finalResult.value}`);
}
// Wrap any function that might throw
const result = chas.tryCatch(
() => JSON.parse('invalid'),
e => e.message
);
const asyncResult = chas.fromPromise(
() => fetch('/data'),
e => e.message
);
// Result matching
const result = await fetchUser(1).match({
ok: user => `Hello, ${user.name}`,
err: err => `Error: ${err}`,
}); // result is typed as `string`
// Data formatting
const result = await chas.shapeAsync({
user: fetchUser(1), // ResultAsync<User, Error>
config: chas.ok({ theme: 'dark' }), // Result<Config, Error>
}); // result is typed as `Result<{ user: User, config: Config }, Error>`
// ResultAsync can be awaited or .then()'d directly since it is a PromiseLike
const result = fetchUser(1); // ResultAsync<User, string>
const thenResult = result.then(user => user.name); // ResultAsync<string, string>
const awaitedResult = await result; // Result<User, string>
// Evaluate multiple ResultAsyncs in parallel
const results1 = await chas.allAsync([fetchUser(1), fetchUser(2)]); // Result<[User, User], Error>
const results2 = await chas.collectAsync([fetchUser(1), fetchUser(2)]); // Result<[User, User], Error[]>
const results3 = await chas.anyAsync([fetchUser(1), fetchUser(2)]); // Result<User, Error[]>
const results4 = await chas.raceAsync([fetchUser(1), fetchUser(2)]); // Result<User, Error>
// ^^^ Each of these has a non-async counterpart that operates on Results instead of ResultAsyncs
// Result and ResultAsync have a built-in pipe method!
const result = chas.ok(1).pipe(add5, double); // Ok(12)
const asyncResult = chas.okAsync(1).pipe(add5, double); // ResultAsync<12, never>
ts-chas/guard)A highly expressive, chainable validation and schema parsing system. Heavily inspired by Zod, but with a focus on type inference and integration with the chas ecosystem.
import { is, defineSchemas, type InferSchema } from 'ts-chas/guard';
// Simple, chainable runtime validation
const isValidEmail = is.string.trim().email.min(5);
if (isValidEmail(someInput)) {
// someInput is correctly inferred as `string` here
}
// Easy object and array validation
const isValidUser = is.object({
name: is.string,
age: is.number.gte(18),
email: is.string.email,
});
const isValidUsers = is.array(isValidUser);
const isValid = isValidUsers(data);
if (isValid) {
// data is correctly inferred as `User[]` here
}
// Schema parsing (supports is.object schemas and base JS objects)
const schemas = defineSchemas({
User: {
id: is.string.uuid('v4'),
age: is.number.gte(18).error('Not old enough!'),
tags: is.array(is.string).min(1),
},
});
// Easily infer a schema's type
type User = InferSchema<typeof schemas.User>;
// Parses unknown data into a Result<{ id: string, age: number, tags: string[] }, GuardErr[]>
const result = schemas.User.parse(unknownData);
if (result.isErr()) {
console.error(result.error.map(e => e.message).join('\n'));
}
// Expressive errors with optional custom messages
const result = schemas.User.parse(unknownData);
if (result.isErr()) {
console.error(result.error); // GuardErr[]
// [{ message: 'Expected string, but got number (123)', path: ['User', 'name'], expected: 'string', schema: 'User' },
// { message: 'Not old enough!', path: ['User', 'age'], expected: 'number', name: 'User' },
// { message: 'Expected string, but got undefined (undefined)', path: ['User', 'email'], expected: 'string', name: 'User' }]
}
Note: All chains returned by
ts-chas/guardnatively implement the Standard Schema v1 specification via the~standardproperty, meaning they can be seamlessly plugged into tRPC, react-hook-form, Drizzle, and other ecosystems.
is Namespace ExtensionsThe Guard API also allows for namespace extensions, allowing you to create custom validators on your own is instance.
const myIs = is.extend({
positiveEven: (v: unknown): v is number => is.number.positive(v) && is.number.even(v),
});
myIs.positiveEven(4); // true
myIs.positiveEven(3); // false
ts-chas/task)A lazy, promise-like wrapper that empowers async operations with functional chaining, retries, dependency injection, and resilience logic limiters. Under the hood, a Task always resolves to a ResultAsync.
import { Task } from 'ts-chas/task';
// Create a task
const fetchTask = Task.from(
() => fetch('/data'),
e => new Error('Fetch failed')
);
// Attach resilience patterns seamlessly
const resilientTask = fetchTask
.retry(3, { delay: 1000, factor: 2 }) // Exponential backoff!
.timeout(5000, () => new Error('Timeout'))
.circuitBreaker({ threshold: 5, resetTimeout: 30000 });
// Execute it when you're ready
const result = await resilientTask.execute();
// Create a task that requires context
interface AppContext {
dbUrl: string;
}
const taskWithContext = Task.ask<AppContext>().chain(ctx =>
Task.from(
() => fetch(`${ctx.dbUrl}/data`),
e => new Error('Failed')
)
);
// Provide the context later (dependency injection)
const readyTask = taskWithContext.provide({ dbUrl: 'https://db.example.com' });
await readyTask.execute();
ts-chas/tagged-errs)Define discriminated unions of native Error instances, allowing exhaustive pattern matching on errors with Result.
import { chas } from 'ts-chas';
const AppError = chas.defineErrs({
NotFound: (resource: string) => ({ resource }),
Unauthorized: () => ({}),
});
const myResult = chas.err(AppError.NotFound('user'));
// Exhaustive error matching!
const message = chas.matchError(myResult.unwrapErr(), {
NotFound: e => `Could not find ${e.resource}`,
Unauthorized: () => 'You lack permissions.',
});
// Catch a specific error type from a Result chain, removing it from the error union
// fetchUser returns Result<User, NotFound | Unauthorized>
const result = await fetchUser(1).catchTag(AppError.NotFound, e => {
console.log(`Could not find ${e.resource}`);
return chas.ok(null);
}); // Result<User | null, Unauthorized>, also works: .catchTag('NotFound', e => ...)
// Tap into a tagged error from a Result chain without modifying the Result
const result = await fetchUser(1).tapTag(AppError.NotFound, e => {
console.log(`Could not find ${e.resource}`);
}); // Result<User, NotFound | Unauthorized>, also works: .tapTag('NotFound', e => ...)
// Check if an unknown value is a tagged error with Guard
const value: unknown = { _tag: 'NotFound', resource: 'user' };
const isNotFoundErr = chas.is.taggedErr(AppError.NotFound)(value);
// also works: chas.is.taggedErr('NotFound')(value);
ts-chas/option)A functional equivalent to nullable values (T | null | undefined). An Option<T> is effectively an alias for Result<NonNullable<T>, never>, tightly integrating with the rest of the library.
import { Option } from 'ts-chas/option';
const maybeUser = Option.fromNullable(getUserOrNull());
// Easily map over valid values
const userName = maybeUser.map(user => user.name).unwrapOr('Anonymous');
ts-chas/pipe)Pipe and Flow are utility functions that allow you to chain functions together in a more readable way. Pipe lets you move data through a pipeline of functions, while Flow lets you compose functions together for later use.
import { pipe, flow } from 'ts-chas/pipe';
const add5 = (x: number) => x + 5;
const double = (x: number) => x * 2;
// Pipe
const result = pipe(1, add5, double); // 12
// Flow
const add5ThenDouble = flow(add5, double);
const result = add5ThenDouble(1); // 12
chas is structured so that its modules communicate deeply with one another. Whether you're moving between Option, Result, Guard, or Task, there are simple pathways built-in.
Converting between types
// ResultAsync to Result
const result = await fetchUser(1); // awaiting a ResultAsync returns a Result
// Result to ResultAsync
const resultAsync = ResultAsync.fromResult(result); // ResultAsync<T, E>
// Result to Option
const maybeData = chas.ok(5).toOption(); // Option<number>
const safelyEmpty = chas.err('fail').toOption(); // Option::None
// Option to Result
const result = Option.toResult(maybeData, () => 'fail'); // Result<number, string>
// Guard to Option
const maybeNumber = Option.fromGuard(value, is.number); // Option<number>
// Option to Task
const task = Task.fromOption(maybeData, () => 'fail'); // Task<number, string>
// Guards to Result-returning function
const validateAge = is
.function({
input: [is.number.gte(18)],
output: is.string,
})
.implResult(value => `You are ${value} years old`);
validateAge(20); // Result.Ok('You are 20 years old')
validateAge(15); // Result.Err(GuardErr)
Guard validations directly to Results:
import { is } from 'ts-chas/guard';
// Convert any guard to a validator that returns a Result
const validateAge = is.number.gte(18).error('Must be 18+').parse;
const result = validateAge(20); // Result.Ok(20)
const errResult = validateAge(15); // Result.Err(GuardErr with msg: 'Must be 18+')
Options into Tasks:
Options are frequently used for synchronous, optional values (such as checking a local cache or reading an environment variable). But when you need to integrate this optional data into a larger, asynchronous pipeline powered by Tasks, converting it directly allows the entire flow to remain linear and cleanly composable without writing clunky .isSome() branches.
import { fromNullable } from 'ts-chas/option';
import { Task } from 'ts-chas/task';
const checkCache = () => fromNullable(localStorage.getItem('user'));
// Upgrade the synchronous Option into an asynchronous Task!
// We provide an error fallback in case the cache (Option) was empty.
const cacheTask = Task.fromOption(checkCache(), () => new Error('Cache miss'));
// We can now seamlessly compose it with other Tasks
const fetchTask = Task.from(
() => fetch('/user'),
() => new Error('Fetch failed')
);
// If it's not in the cache, fallback to fetching it
const userTask = cacheTask.orElse(() => fetchTask);
const userResult = await userTask.execute();
Generators mapping everything:
With chas.go, you can mix and match Result and ResultAsync calls in a clean, imperative-looking flow without infinite .andThen chains.
const userProfileResult = await chas.go(async function* () {
// Yield a ResultAsync
const user = yield* fetchUserAsync();
// Yield a synchronous Result
const config = yield* parseConfig(user.prefs);
return { user, config };
}); // if ok, data is typed as { user, config }, if err, it's typed as the union of all possible errors
// Also possible with Task! (orchestrates Task instances)
const userProfileTask = Task.go(async function* () {
const user = yield* fetchUserTask();
const config = yield* parseConfigTask(user.prefs);
return { user, config };
});
const userProfileResult = await userProfileTask.execute();
npm install ts-chas
# or
yarn add ts-chas
# or
pnpm add ts-chas
I'm always open to contributions! Feel free to open an issue or submit a PR.
FAQs
A functional programming and utility library for TypeScript that provides tools for explicit error handling, runtime validation, and asynchronous operations.
We found that ts-chas demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.