The decoders library uses Result types to represent the outcome of decoding operations. This provides a type-safe way to handle both successful and failed validations without throwing exceptions.
Result<T, E>
A discriminated union type representing either a successful result or an error.
type Result<T, E> = Ok<T> | Err<E>
Type Parameters
The type of the success value
The type of the error value
Ok<T>
Represents a successful computation.
type Ok<T> = {
readonly ok: true;
readonly value: T;
readonly error?: never;
}
Properties
Discriminant property that’s always true for successful results
The successful result value
Always undefined for successful results (TypeScript will prevent access)
Err<E>
Represents a failed computation.
type Err<E> = {
readonly ok: false;
readonly value?: never;
readonly error: E;
}
Properties
Discriminant property that’s always false for error results
Always undefined for error results (TypeScript will prevent access)
DecodeResult<T>
A specialized Result type used by decoders, where errors are always Annotation objects.
type DecodeResult<T> = Result<T, Annotation>
This is equivalent to:
type DecodeResult<T> = Ok<T> | Err<Annotation>
Constructor Functions
ok()
Creates a successful Result.
Returns: Ok<T>
import { ok } from 'decoders';
const result = ok(42);
console.log(result.ok); // true
console.log(result.value); // 42
console.log(result.error); // undefined
err()
Creates an error Result.
Returns: Err<E>
import { err } from 'decoders';
const result = err('Something went wrong');
console.log(result.ok); // false
console.log(result.value); // undefined
console.log(result.error); // 'Something went wrong'
// With Error object
const result2 = err(new Error('Validation failed'));
console.log(result2.ok); // false
console.log(result2.error); // Error object
Usage Examples
Pattern Matching with Type Guards
The ok property acts as a discriminant for type-safe pattern matching:
import { string } from 'decoders';
const result = string.decode(input);
if (result.ok) {
// TypeScript knows this is Ok<string>
console.log(result.value); // string
// result.error // ❌ TypeScript error - property doesn't exist
} else {
// TypeScript knows this is Err<Annotation>
console.log(result.error); // Annotation
// result.value // ❌ TypeScript error - property doesn't exist
}
Handling Results
import { number } from 'decoders';
function processNumber(input: unknown): number | null {
const result = number.decode(input);
if (result.ok) {
return result.value;
}
console.error('Validation failed:', result.error);
return null;
}
console.log(processNumber(42)); // 42
console.log(processNumber('not a number')); // null
Accessing Result Properties
import { ok, err } from 'decoders';
const success = ok(42);
console.log(success.ok); // true
console.log(success.value); // 42
console.log(success.error); // undefined
const failure = err('Oops');
console.log(failure.ok); // false
console.log(failure.value); // undefined
console.log(failure.error); // 'Oops'
Using in Custom Decoders
import { define, type DecodeResult } from 'decoders';
const positiveNumber = define((blob, ok, err): DecodeResult<number> => {
if (typeof blob !== 'number') {
return err('Must be a number');
}
if (blob <= 0) {
return err('Must be positive');
}
return ok(blob);
});
const result = positiveNumber.decode(5);
if (result.ok) {
console.log('Valid:', result.value);
}
Use the .value property of the Result, which is undefined for errors:
import { string } from 'decoders';
const result1 = string.decode('hello');
const result2 = string.decode(123);
console.log(result1.value); // 'hello'
console.log(result2.value); // undefined
Or use the decoder’s .value() method for convenience:
import { string } from 'decoders';
const value1 = string.value('hello'); // 'hello'
const value2 = string.value(123); // undefined
Comparison with Exceptions
Traditional Exception Handling
try {
const value = string.verify(input);
console.log('Success:', value);
} catch (error) {
console.error('Failed:', error);
}
Result-Based Handling
const result = string.decode(input);
if (result.ok) {
console.log('Success:', result.value);
} else {
console.error('Failed:', result.error);
}
Benefits of Result Types
Type Safety
TypeScript enforces that you handle both success and error cases:
import { number } from 'decoders';
const result = number.decode(input);
// ❌ TypeScript error - value might not exist
const x = result.value.toFixed(2);
// ✅ Correct - check the discriminant first
if (result.ok) {
const x = result.value.toFixed(2);
}
No Exceptions in Happy Path
Results allow you to handle validation without try-catch blocks:
import { object, string, number } from 'decoders';
const userDecoder = object({
name: string,
age: number,
});
const result = userDecoder.decode(data);
if (result.ok) {
// Use the validated data
saveUser(result.value);
} else {
// Handle the error
return { error: formatError(result.error) };
}
Composability
Results can be easily composed and transformed:
import { string, number } from 'decoders';
function parseNumbers(inputs: unknown[]): number[] {
const results = inputs.map(input => number.decode(input));
// Filter out errors
return results
.filter(r => r.ok)
.map(r => r.value);
}
// Or fail fast on first error
function parseNumbersStrict(inputs: unknown[]): number[] | null {
const numbers: number[] = [];
for (const input of inputs) {
const result = number.decode(input);
if (!result.ok) {
return null; // Stop on first error
}
numbers.push(result.value);
}
return numbers;
}
Decoder<T> - The decoder class that produces DecodeResults
Annotation - The error type used in DecodeResults (see formatting documentation)
See Also
- Decoder Class - Methods for working with decoders
- define() - Creating custom decoders that return Results