@guanriyue/decurl/decode

@guanriyue/decurl/decode provides optional decode primitives. They compose common string parsing logic, but they are not required; complex structures can use a custom decode function directly.

import { pipe, shape, toNumber, trim } from '@guanriyue/decurl/decode';

Failure Semantics

Decode<TInput, TOutput> is a synchronous function. Returning null or undefined means parsing failed, and pipe stops later steps.

import type { Decode } from '@guanriyue/decurl/decode';

const compact: Decode<string, string> = (value) => {
  const nextValue = value.trim();
  return nextValue.length > 0 ? nextValue : undefined;
};

Composition

Composition helpers organize a decode pipeline.

mapItems

mapItems(...steps) runs pipe-like decode logic for every array item. Items that fail to parse are filtered out.

import { mapItems, pipe, shape, toNumber, unique } from '@guanriyue/decurl/decode';

const decodeIds = pipe(
  mapItems(shape.integer, toNumber),
  unique,
);

pipe

pipe(...steps) runs multiple decode steps in order. The input type comes from the first step, and the output type comes from the last step.

When a step returns null or undefined, pipe stops later steps and returns undefined.

import { min, pipe, shape, toNumber, trim } from '@guanriyue/decurl/decode';

const decodePage = pipe(trim, shape.integer, toNumber, min(1));

Normalize

Normalize APIs arrange raw strings into a better shape for later parsing.

trim

trim(input) removes whitespace from both ends of a string.

import { trim } from '@guanriyue/decurl/decode';

trim('  decurl  ');
// 'decurl'

trim.end

trim.end(input) removes trailing whitespace.

trim.end('  decurl  ');
// '  decurl'

trim.start

trim.start(input) removes leading whitespace.

trim.start('  decurl  ');
// 'decurl  '

Shape / Guard

Shape and Guard APIs only check whether a value satisfies a requirement. They do not convert the value. If the requirement is not met, they return undefined.

elementOf

elementOf(values) requires the value to be included in the given array. It uses Object.is for membership checks and does not perform type conversion.

import { elementOf, pipe, shape, toNumber } from '@guanriyue/decurl/decode';

const decodePageSize = pipe(
  shape.integer,
  toNumber,
  elementOf([20, 50, 100]),
);

elementOf(enumDefinition)

elementOf(enumDefinition) requires the value to be one of the enum definition values. For numeric enums, usually convert to number first.

enum PageSize {
  Small = 20,
  Large = 50,
}

const decodePageSize = pipe(shape.integer, toNumber, elementOf(PageSize));

shape

shape(regexp) requires the string to match the given regular expression. The output is still the original string.

import { shape } from '@guanriyue/decurl/decode';

const nonEmpty = shape(/.+/);

shape.boolish

shape.boolish requires the string to be 'true' or 'false', and narrows the type to 'true' | 'false'.

import { pipe, shape, toBoolean } from '@guanriyue/decurl/decode';

const decodeEnabled = pipe(shape.boolish, toBoolean);

shape.date

shape.date requires the string to be YYYY-MM-DD and checks that the date is real.

shape.date('2026-02-30');
// undefined

shape.datetime

shape.datetime requires the string to match a common date-time shape and checks the date and time ranges.

shape.datetime('2026-06-07T13:45:30Z');
// '2026-06-07T13:45:30Z'

shape.integer

shape.integer requires the string to match a decimal integer shape. It does not convert the string to number.

import { pipe, shape, toNumber } from '@guanriyue/decurl/decode';

const decodeInteger = pipe(shape.integer, toNumber);

shape.month

shape.month requires the string to be YYYY-MM and checks the month range.

shape.month('2026-06');
// '2026-06'

shape.number

shape.number requires the string to match a decimal number shape. It does not convert the string to number.

const decodeNumber = pipe(shape.number, toNumber);

startsWith

startsWith(prefix) requires the string to start with the prefix and narrows the type to a template literal type.

import { startsWith } from '@guanriyue/decurl/decode';

const decodeUserId = startsWith('user_');

where

where(predicate) filters a value with a predicate or type guard. When the predicate returns false, the decode result is undefined.

import { where } from '@guanriyue/decurl/decode';

const positive = where((value: number) => value > 0);

Convert

Convert APIs transform a value into a new type.

toBoolean

toBoolean(input) converts 'true' / 'false' into boolean. Other inputs return undefined.

import { shape, pipe, toBoolean } from '@guanriyue/decurl/decode';

const decodeBoolean = pipe(shape.boolish, toBoolean);

toNumber

toNumber(input) converts with Number(input) and requires the result to be a finite number.

import { shape, pipe, toNumber } from '@guanriyue/decurl/decode';

const decodeNumber = pipe(shape.number, toNumber);

Constraint

Constraint APIs continue narrowing the valid range. They only check constraints and do not change the input value.

length

length(size) requires the input length to equal the given value.

length([min, max]) requires the input length to be in the closed interval.

import { length, pipe, trim } from '@guanriyue/decurl/decode';

const decodeCode = pipe(trim, length(6));
const decodeKeyword = pipe(trim, length([1, 40]));

length.max

length.max(value) requires the input length to be less than or equal to the given value.

const decodeKeyword = pipe(trim, length.max(40));

length.min

length.min(value) requires the input length to be greater than or equal to the given value.

const decodeKeyword = pipe(trim, length.min(1));

max

max(value) requires a number to be less than or equal to the given value. It does not change the input; if the constraint is not met, it returns undefined.

import { max, pipe, shape, toNumber } from '@guanriyue/decurl/decode';

const decodePercent = pipe(shape.number, toNumber, max(100));

min

min(value) requires a number to be greater than or equal to the given value. It does not change the input; if the constraint is not met, it returns undefined.

import { min, pipe, shape, toNumber } from '@guanriyue/decurl/decode';

const decodePage = pipe(shape.integer, toNumber, min(1));

Array

Array APIs handle arrays as a whole.

unique

unique(input) removes duplicates with Object.is and preserves the first occurrence order.

import { mapItems, pipe, shape, toNumber, unique } from '@guanriyue/decurl/decode';

const decodeIds = pipe(
  mapItems(shape.integer, toNumber),
  unique,
);

unique.by

unique.by(identity) removes duplicates by the identity result.

import { unique } from '@guanriyue/decurl/decode';

const uniqueById = unique.by((item: { id: number }) => item.id);