The define() function allows you to create custom Decoder<T> instances with your own validation logic.
Type Signature
function define<T>(fn: AcceptanceFn<T>): Decoder<T>
type AcceptanceFn<O, I = unknown> = (
blob: I,
ok: (value: O) => DecodeResult<O>,
err: (msg: string | Annotation) => DecodeResult<O>,
) => DecodeResult<O>
Parameters
An acceptance function that receives three arguments:
blob - The untrusted input to validate
ok - Call ok(value) to accept the input and return that value
err - Call err(message) to reject the input with an error message
The function must return a DecodeResult<T> by calling either ok() or err().
Returns: Decoder<T> - A new decoder instance
Basic Usage
import { define } from 'decoders';
// Simple decoder that accepts only the number 123
const only123 = define((blob, ok, err) => {
if (blob === 123) {
return ok(123);
}
return err('Must be 123');
});
console.log(only123.verify(123)); // 123
// only123.verify(456) throws error
Multiple Acceptance Paths
You can accept different types of input and normalize them:
import { define } from 'decoders';
const flexibleDate = define((blob, ok, err) => {
// Accept number 123
if (blob === 123) {
return ok(123);
}
// Accept 'now' string as current date
if (blob === 'now') {
return ok(new Date());
}
// Reject everything else
return err('Must be 123 or "now"');
});
console.log(flexibleDate.verify(123)); // 123
console.log(flexibleDate.verify('now')); // Date object
// flexibleDate.verify('other') throws error
Type-Safe Parsing
Create decoders that parse and transform input:
import { define } from 'decoders';
// Parse hex strings to numbers
const hexNumber = define<number>((blob, ok, err) => {
// First validate it's a string
if (typeof blob !== 'string') {
return err('Must be a string');
}
// Try to parse as hex
const num = parseInt(blob, 16);
if (Number.isNaN(num)) {
return err('Must be a valid hex string');
}
return ok(num);
});
console.log(hexNumber.verify('FF')); // 255
console.log(hexNumber.verify('DEADC0DE')); // 3735929054
// hexNumber.verify('not-hex') throws error
Composing with Existing Decoders
You can use existing decoders within your custom decoder:
import { define, string, number } from 'decoders';
// Accept either a number or a numeric string
const flexibleNumber = define<number>((blob, ok, err) => {
// If it's already a number, accept it
const numResult = number.decode(blob);
if (numResult.ok) {
return ok(numResult.value);
}
// If it's a string, try to parse it
const strResult = string.decode(blob);
if (strResult.ok) {
const parsed = Number(strResult.value);
if (!Number.isNaN(parsed)) {
return ok(parsed);
}
}
return err('Must be a number or numeric string');
});
console.log(flexibleNumber.verify(42)); // 42
console.log(flexibleNumber.verify('42')); // 42
// flexibleNumber.verify('abc') throws error
Advanced: Using Annotations
For more control over error formatting, you can use Annotation objects:
import { define, annotate } from 'decoders';
const positiveNumber = define<number>((blob, ok, err) => {
if (typeof blob !== 'number') {
return err(annotate(blob, 'Must be a number'));
}
if (blob <= 0) {
return err(annotate(blob, 'Must be positive'));
}
return ok(blob);
});
Error Handling
The err() function accepts either a string or an Annotation object:
import { define, annotate } from 'decoders';
const myDecoder = define((blob, ok, err) => {
// Simple string error
if (condition1) {
return err('Simple error message');
}
// Annotation for more control
if (condition2) {
return err(annotate(blob, 'Error with context'));
}
return ok(blob);
});
Important Notes
The ok() and err() functions do not perform side effects. You must return their values.
// ❌ WRONG - doesn't return the result
const bad = define((blob, ok, err) => {
if (blob === 123) {
ok(123); // Missing return!
}
});
// ✅ CORRECT - returns the result
const good = define((blob, ok, err) => {
if (blob === 123) {
return ok(123);
}
return err('Not 123');
});
Building Validators
Combine define() with decoder methods to create reusable validators:
import { define, string } from 'decoders';
// Email-like validator (simplified)
const email = define((blob, ok, err) => {
if (typeof blob !== 'string') {
return err('Must be a string');
}
if (!blob.includes('@')) {
return err('Must be a valid email');
}
return ok(blob);
}).transform(s => s.toLowerCase());
// URL validator
const url = define((blob, ok, err) => {
if (typeof blob !== 'string') {
return err('Must be a string');
}
try {
const parsed = new URL(blob);
return ok(parsed.href);
} catch {
return err('Must be a valid URL');
}
});
All Decoder Methods Available
Decoders created with define() have access to all decoder methods:
import { define } from 'decoders';
const baseDecoder = define((blob, ok, err) => {
return typeof blob === 'string' ? ok(blob) : err('Not a string');
});
// Chain additional validations
const email = baseDecoder
.refine(s => s.includes('@'), 'Must contain @')
.transform(s => s.toLowerCase())
.describe('Must be a valid email');