Option<T> models the explicit presence (Some) or absence (None) of a value. Under the hood, it is an alias for Result<NonNullable<T>, never>, which means it shares the full Result API: .map(), .andThen(), .unwrapOr(), and everything else.
import { nullable } from 'ts-chas/option';
// Instead of:
const user = getUserFromCache('123'); // User | null
if (user) {
console.log(user.name);
}
// You get:
const maybeUser = nullable(getUserFromCache('123')); // Option<User>
const name = maybeUser.map((u) => u.name).unwrapOr('Anonymous');
Creating options
nullable(value)
Converts any nullable value (T | null | undefined) into an Option<T>:
import { nullable } from 'ts-chas/option';
const a = nullable('hello'); // Some('hello')
const b = nullable(null); // None
const c = nullable(undefined); // None
optionFromGuard(value, guard)
Creates an Option from an unknown value and a type guard. Returns Some if the guard passes, None otherwise:
import { optionFromGuard } from 'ts-chas/option';
import { is } from 'ts-chas/guard';
const maybeNum = optionFromGuard(someValue, is.number);
// Some<number> if someValue is a number, None otherwise
The full Result API
Because Option<T> is Result<NonNullable<T>, never>, every Result method is available:
const greeting = maybeUser
.map((user) => user.firstName)
.map((name) => `Hello, ${name}!`)
.unwrapOr('Hello, Guest!');
// Chain fallible operations
const verified = maybeToken
.andThen((token) => validateToken(token)) // returns Option<Claims>
.unwrapOr(null);
Converting to Result
Use .orElse() to assign a specific error to the None case, converting an Option to a Result:
import { nullable } from 'ts-chas/option';
import { chas } from 'ts-chas';
const maybeUser = nullable(getUserFromCache(id));
// Provide a concrete error for the None branch
const result = maybeUser.orElse(() => chas.err('User not found in cache'));
// Result<User, string>
Upgrading to Task
Option is ideal for synchronous lookups (like reading a cache or an environment variable). When you need to feed that optional value into an async pipeline, upgrade it to a Task with Task.fromOption():
import { nullable } from 'ts-chas/option';
import { Task } from 'ts-chas/task';
// Check cache first (synchronous Option)
const maybeUser = nullable(localStorage.getItem('cached-user'));
// Promote to Task — provide an error for the None case
const cacheTask = Task.fromOption(maybeUser, () => new Error('Cache miss'));
// Compose with a fetch Task as fallback
const fetchTask = Task.from(
() => fetch('/api/user').then((r) => r.json()),
() => new Error('Fetch failed')
);
const userTask = cacheTask.orElse(() => fetchTask);
const result = await userTask.execute();
// Result<string, Error>
Task.fromOption() is the idiomatic way to bridge synchronous optional values into larger async workflows. The None case becomes an Err that your Task pipeline can handle or recover from.
Practical example: cache-then-fetch
import { nullable } from 'ts-chas/option';
import { Task } from 'ts-chas/task';
interface User {
id: string;
name: string;
}
function getUserFromCache(id: string): User | null {
const raw = sessionStorage.getItem(`user:${id}`);
return raw ? JSON.parse(raw) : null;
}
function fetchUser(id: string): Task<User, Error> {
return Task.from(
() => fetch(`/api/users/${id}`).then((r) => r.json()),
(e) => new Error(`Fetch failed: ${e}`)
);
}
async function getUser(id: string) {
const cachedUser = nullable(getUserFromCache(id));
const userTask = Task.fromOption(cachedUser, () => new Error('Not in cache'))
.orElse(() => fetchUser(id));
const result = await userTask.execute();
return result.match({
ok: (user) => user,
err: (e) => { throw e; },
});
}