Skip to main content
The modules in ts-chas are built to interoperate. This page shows the conversion paths you’ll use most often, why you’d reach for each one, and exactly how to write them.

Result ↔ Option

Option<T> is an alias for Result<NonNullable<T>, never>, so converting between the two is lossless in terms of your value. You’re only deciding how you want to represent the absence of data. Use result.toOption() when you’ve already handled the error in context and you want to treat an Err as simply “nothing here”. The error payload is discarded.
import { chas } from 'ts-chas';

const parsed = chas.ok(42);
const option = parsed.toOption(); // Option<number> — Some(42)

const failed = chas.err('not found');
const empty = failed.toOption(); // Option::None — error is dropped
Use .orElse() when you’re integrating an optional value into a pipeline that requires a concrete error type. You provide the error so callers know why the value was absent.
import { nullable } from 'ts-chas/option';
import { chas } from 'ts-chas';

const maybeId = nullable(localStorage.getItem('userId'));

// Promote the Option into a Result with a descriptive error
const result = maybeId.orElse(() => chas.err('userId not found in storage'));
// Result<string, string>

Guard → Result

Guards are type predicates by default, but every guard chain also exposes a .parse() method that returns a Result. This is the right choice whenever you want structured validation errors that your callers can handle programmatically rather than relying on thrown exceptions. Use .parse() directly on any guard to validate a single value:
import { is } from 'ts-chas/guard';

const emailResult = is.string.trim().email.min(5).parse(userInput);
// Result<string, GuardErr>

if (emailResult.isErr()) {
  console.error(emailResult.error.message);
  // e.g. "Expected a valid email address"
}
Use is.function(...).implResult() when you have a function whose inputs and outputs should be validated, and you want validation failures to surface as typed Result errors instead of unchecked exceptions:
import { is } from 'ts-chas/guard';

// Validate inputs and output — the function itself only runs if all guards pass
const validateAge = is
  .function({
    input: [is.number.gte(18).error('Must be 18 or older')],
    output: is.string,
  })
  .implResult((age) => `You are ${age} years old`);

const ok = validateAge(25);   // Result.Ok('You are 25 years old')
const err = validateAge(15);  // Result.Err(GuardErr { message: 'Must be 18 or older' })

Option → Task

Option models synchronous optional values: a cache lookup, an environment variable, a local-storage read. Task orchestrates asynchronous pipelines. Task.fromOption() bridges the two so your cache check integrates cleanly into an async flow without branching on .isSome(). Use Task.fromOption() when you want to treat a cache hit as the success path and fall through to an async fetch on a miss:
import { nullable } from 'ts-chas/option';
import { Task } from 'ts-chas/task';

const checkCache = () => nullable(localStorage.getItem('user'));

// Promote the synchronous Option into a lazy Task
const cacheTask = Task.fromOption(
  checkCache(),
  () => new Error('Cache miss')
);

const fetchTask = Task.from(
  () => fetch('/api/user').then((r) => r.text()),
  () => new Error('Fetch failed')
);

// If the cache is empty, fall back to the network request
const userTask = cacheTask.orElse(() => fetchTask);
const result = await userTask.execute();
// Result<string, Error>
The .orElse() callback only runs when cacheTask produces an Err (i.e., when the cache was empty). A cache hit short-circuits the whole chain.

Result / ResultAsync → ResultAsync

You often work in a context where some helpers return synchronous Result and others return ResultAsync. ResultAsync.fromResult() lifts a synchronous Result into the async context so you can chain .andThen(), .map(), and other async operators uniformly.
import { chas } from 'ts-chas';

function parseConfig(raw: string): chas.Result<Config, string> {
  try {
    return chas.ok(JSON.parse(raw) as Config);
  } catch {
    return chas.err('Invalid JSON in config');
  }
}

// Lift into ResultAsync so you can continue an async chain
const configAsync = chas.ResultAsync.fromResult(parseConfig(rawConfigString));

const pipeline = configAsync
  .andThen((config) => fetchUserAsync(config.userId)) // ResultAsync
  .map((user) => user.displayName);

const result = await pipeline; // Result<string, string>

chas.go() — mixing Result and ResultAsync

.andThen() chains are clean for two or three steps, but deeper pipelines become hard to read. chas.go() lets you write the same logic imperatively using async function* generators. Each yield* either extracts the Ok value or short-circuits by returning the Err. You can freely mix synchronous Result and ResultAsync in the same generator.
import { chas } from 'ts-chas';

declare function fetchUserAsync(id: string): chas.ResultAsync<User, NetworkError>;
declare function parseConfig(prefs: string): chas.Result<Config, ParseError>;
declare function saveProfileAsync(user: User, config: Config): chas.ResultAsync<void, DbError>;

const result = await chas.go(async function* () {
  // yield* unwraps Ok or short-circuits with the Err
  const user = yield* fetchUserAsync('user-123');       // ResultAsync
  const config = yield* parseConfig(user.prefs);        // Result
  yield* saveProfileAsync(user, config);                // ResultAsync

  return { user, config };
});
// ResultAsync<{ user: User; config: Config }, NetworkError | ParseError | DbError>

if (result.isOk()) {
  console.log(`Saved profile for ${result.value.user.name}`);
}
The final type collects the union of all possible error types from every yield* call, so TypeScript forces you to handle every failure mode.

Guard + Tagged Errors

When you receive an unknown value from an external boundary (a caught exception, a message bus event, an inter-frame message), you need to check whether it’s a specific tagged error before acting on it. is.tagged() from ts-chas/guard gives you a type-safe guard for exactly this. Use is.tagged() when you’re catching an unknown thrown value and need to branch on a specific error variant:
import { is } from 'ts-chas/guard';
import { chas } from 'ts-chas';

const AppError = chas.defineErrs({
  NotFound: (resource: string) => ({ resource }),
  Unauthorized: () => ({}),
});

// Check an unknown value against a specific error tag
function handleUnknown(value: unknown): string {
  if (is.tagged(AppError.NotFound)(value)) {
    // `value` is now typed as NotFoundErr
    return `Resource not found: ${value.resource}`;
  }
  if (is.tagged(AppError.Unauthorized)(value)) {
    return 'You are not authorized';
  }
  return 'Unknown error';
}
You can also pass a plain string tag when you don’t have the factory in scope:
if (is.tagged('NotFound')(value)) {
  // value is typed as { _tag: string } & Error
}