Writing your own shapes β
The simplest custom shape β
Let's say you want to convert empty strings to null.
export const emptyStringToNull = (x: string): string | null =>
x.trim() === "" ? null : x;That's already a valid shape.
Use it:
const mapper = compileMapper<Source, Destination>({
description: transform(emptyStringToNull),
});Handling null and undefined β
A very common need: convert undefined | T into T | null.
Mapia already gives you helpers, but let's understand how this works.
Conceptually β
If value is missing β return
nullOtherwise β transform it
Using nullableShapeFrom β
import { nullableShapeFrom, numberShape } from "mapia";
const nullableNumber = nullableShapeFrom(numberShape);
nullableNumber("42"); // 42
nullableNumber(undefined); // null
nullableNumber(null); // nullThis is still just a function:
(x) => number | nullWriting your own nullable shape (manual version) β
If you don't want to use helpers yet:
export const nullableNumberShape = (
x: string | number | null | undefined
): number | null => {
if (x == null) return null;
return Number(x);
};This is perfectly fine.
Later, helpers just remove repetition - they don't change the concept.
Shapes can be composed β
Because shapes are functions, you can compose them mentally:
string -> number -> nullExample:
export const safePositiveNumber = (x: unknown): number | null => {
const n = Number(x);
if (Number.isNaN(n) || n < 0) return null;
return n;
};Use it directly:
transform(safePositiveNumber)Shapes vs validation libraries β
Shapes are not validators.
- They do not check
- They do not report errors
- They do not reject input
They convert.
If your shape signature cannot satisfy all cases of the Output, you choose:
- return
null - return
undefined - throw
- return a default value
That decision belongs to the shape author.
Advanced shapes β
Mapia relies heavily on the concepts of functional programming. There are concepts that are agnostic to a language:
- Pure Functions:
Always returns the same output for the same input
- Immutability
Data is not changed after itβs created. Instead of modifying data, you create new versions
- First-Class & Higher-Order Functions:
Functions can be stored in variables, be passed as arguments, be returned from other functions
To write shapes like a pro, you can read more about functional programming in Typescript
In this block, we will only discuss concepts of Mapia
Shapes are built using a small Either abstraction:
(input) => Either<Error, Output>From decoder to reusable shape β
A decoder answers one question only:
βCan I convert this value or not?β
A shape answers a second question:
βWhat should I do if conversion fails?β
Mapia keeps these concerns separate on purpose.
Letβs walk through the pattern used in Mapiaβs own shapes.
Example: urlOrNullShape β
Step 1: write a decoder β
A decoder never throws. It only reports success or failure.
export const urlDecoder: Decoder<string, URL> = (x) => {
try {
return right(new URL(x));
} catch (error) {
return left(error as Error);
}
};This decoder:
- succeeds with
Right(URL) - fails with
Left(Error)s
Step 2: decide failure policy β
Now decide what failure means.
For urlOrNullShape, the policy is:
If parsing fails β return
null
export const urlOrNullShape =
leftToNull(composeDecoder(tryNonNullable(), urlDecoder));What happens here conceptually:
tryNonNullable()fails early if value isnullorundefinedurlDecodertries to parse the URLleftToNullconverts any failure intonull
Final shape type:
(input: string | null | undefined) => URL | nullUsing the shape in a mapper β
const mapper = compileMapper<ApiUser, User>({
website: transform(urlOrNullShape),
});