Normul is a tiny TypeScript/JavaScript library for fearless, predictable data normalization and transformation. No validation exceptions, no magic, just pure, explicit conversion of anything into the shape you want. Use it to make your data boringly consistent.
Normul is a data transformer and normalizer, not a validator. Unlike Zod, Valibot, and similar libraries, Normul doesn't check if your data is "valid" β it just forcefully and predictably transforms whatever you give it into the shape you want. If something doesn't fit, Normul will do its best to convert it, not throw errors or stop your app.
- π Quick Start
- π¦ Installation
- π§ Core Concepts
- π οΈ Schema Factories
- βοΈ Schema Modifiers
- π§° Utils
- π‘ Examples
- Why Normul Is Different
import * as n from "normul";
// Define your schema
const userSchema = n.object({
id: n.number,
name: n.string,
isActive: n.boolean,
tags: n.array(n.string),
});
// Process any data (even messy data!)
const messyData = {
id: "42", // string, not number
name: null, // null, not string
isActive: 1, // number, not boolean
tags: "admin", // string, not array
};
// Get normalized data
const { data } = userSchema.normalize(messyData);
console.log(data);
// {
// id: 42,
// name: "",
// isActive: true,
// tags: ["admin"],
// }
// TypeScript type inference: get the normalized data type
type User = n.Infer<typeof userSchema>;
// {
// id: number;
// name: string;
// isActive: boolean;
// tags: string[];
// }
# Using npm
npm install normul
# Using yarn
yarn add normul
# Using pnpm
pnpm add normul
Schemas are the building blocks of Normul. The way schemas are built is almost the same as in many other libraries. Each schema defines how a specific type of data should be normalized.
// Create schemas using factory functions
const stringSchema = n.string;
const numberSchema = n.number;
const userSchema = n.object({
name: n.string,
age: n.number,
});
And of course, you can nest schemas as deeply as you want without any limitations
const nestedSchema = n.object({
nested: n.array(
n.object({
a: n.object({
b: n.number.nullable,
}),
}),
n.object({
c: n.object({
d: n.object({
e: n.number.optional,
}),
}),
}),
),
});
type Nested = n.Infer<typeof nestedSchema>;
// {
// nested: ({
// a: {
// b: number | null;
// };
// } | {
// c: {
// d: {
// e?: number;
// };
// };
// })[];
// }
Normalization is the process of transforming input data to match a schema. Unlike validation libraries that throw errors for mismatched data, Normul always tries to convert the data:
// Normalizes anything to a string
const stringResult = n.string.normalize(42);
console.log(stringResult.data); // "42"
// Normalizes anything to a number
const numberResult = n.number.normalize("42.5");
console.log(numberResult.data); // 42.5
When normalization involves type conversion or other transformations, Normul tracks these as "issues":
const { data, issues } = n.string.normalize(42);
console.log(data); // "42"
console.log(issues);
// [
// {
// path: [],
// message: "Converted to string",
// level: "warn",
// expected: "string",
// received: 42,
// }
// ]
Issue levels:
info
: Informational messageswarn
: Warnings about non-exact matches that were convertederror
: Errors that occurred during normalization
Converts everything to string.
// Converts primitives to their string equivalents
n.string.normalize(42).data // "42"
n.string.normalize(true).data // "true"
// Converts empty values to empty strings
n.string.normalize(undefined).data // ""
n.string.normalize(null).data // ""
// Converts objects/arrays to JSON
n.string.normalize([1, 2, 3]).data // "[1,2,3]"
n.string.normalize({ a: 123 }).data // '{"a":123}'
// Handles circular references gracefully
const a = {}; a.b = a;
n.string.normalize(a).data // [object Object]
Converts everything to number.
// Converts primitives to their number equivalents
n.number.normalize("42").data // 42
n.number.normalize(true).data // 1
// Converts non-numbers and non-finite numbers to 0
n.number.normalize(null).data // 0
n.number.normalize({ a: 123 }).data // 0
n.number.normalize(NaN).data // 0
n.number.normalize(Infinity).data // 0
Converts everything to boolean.
// Converts common string representations to boolean
n.boolean.normalize("False").data // false
n.boolean.normalize("0").data // false
n.boolean.normalize("NO").data // false
n.boolean.normalize("TRUE").data // true
n.boolean.normalize("1").data // true
n.boolean.normalize("yes").data // true
// Converts falsy primitives to false
n.boolean.normalize(0).data // false
n.boolean.normalize(null).data // false
n.boolean.normalize(undefined).data // false
n.boolean.normalize("").data // false
// Converts truthy values to true
n.boolean.normalize(1).data // true
n.boolean.normalize(-1).data // true
n.boolean.normalize(" ").data // true
n.boolean.normalize({}).data // true
n.boolean.normalize([]).data // true
Converts everything to null.
Note: Due to JavaScript limitations, the schema is called nullValue
instead of null
.
n.nullValue.normalize(anything).data // null
Converts everything to undefined.
Note: Due to JavaScript limitations, the schema is called undefinedValue
instead of undefined
.
n.undefinedValue.normalize(anything).data // undefined
Always returns the specified value.
n.literal("active").normalize(anything).data // "active"
Transforms objects according to the specified schema.
const userSchema = n.object({
name: n.string,
age: n.number,
});
userSchema.normalize({ name: "Alice", age: "30" }).data
// { name: "Alice", age: 30 }
// At least somehow tries to transform data so the type matches
userSchema.normalize("foo").data
// { name: "", age: 0 }
Creates arrays of normalized elements.
const tagsSchema = n.array(n.string);
tagsSchema.normalize(["admin", 123, true]).data
// ["admin", "123", "true"]
// Wraps single items in an array
tagsSchema.normalize("admin").data
// ["admin"]
// Array elements can have different schemas, in this case the most suitable one is selected or the first one
const numberOrStringArraySchema = n.array(n.string, n.number);
numberOrStringArraySchema.normalize(["foo", 123, true]).data
// ["foo", 123, "true"]
// TypeScript recognizes such schemas as union arrays
type NumberOrStringArray = n.Infer<typeof numberOrStringArraySchema>;
// (string | number)[]
Creates record with string values from any object properties.
const metadataSchema = n.record(n.string);
metadataSchema.normalize({ age: 42, active: true }).data
// { age: "42", active: "true" }
// You can also specify a schema for the key by passing two schemas, the first of which is the key
const numberRecordSchema = n.record(n.number, n.boolean);
numberRecordSchema.normalize({ "foo": 1 }).data
// { "0": true }
// TypeScript recognizes such schemas as records
type NumberRecord = n.Infer<typeof numberRecordSchema>;
// Record<number, boolean>
Creates fixed-length array with different element types.
const pointSchema = n.tuple(n.number, n.number, n.string);
pointSchema.normalize([1, "2", true]).data
// [1, 2, "true"]
// Wraps single items in an array and fills the rest
const stringTupleSchema = n.tuple(n.string, n.string);
stringTupleSchema.normalize("foo").data
// ["foo", ""]
// TypeScript recognizes such schemas as tuples
type Point = n.Infer<typeof pointSchema>;
// [number, number, string]
Tries schemas in order, picks first that fits best.
const flexibleId = n.union(n.number, n.string);
flexibleId.normalize(42).data // 42
flexibleId.normalize("abc").data // "abc"
Picks schema based on field value.
const shapes = n.discriminatedUnion(
"type",
n.object({
type: n.literal("circle"),
radius: n.number
}),
n.object({
type: n.literal("rectangle"),
width: n.number,
height: n.number
})
);
shapes.normalize({
type: "circle",
radius: "5"
}).data
// { type: "circle", radius: 5 }
Defines a schema for any custom TypeScript type. This does not perform any transformation by itself, but allows you to specify the output type explicitly.
n.type<YourCustomType>().normalize(input).data
Specifies the type as any
. Equivalent to using .type<any>()
.
n.any.normalize(42).data // 42 (typed as any)
Specifies the type as unknown
. Equivalent to using .type<unknown>()
.
n.unknown.normalize(42).data // 42 (typed as unknown)
Allows undefined values. Note: This changes the schema type.
const optionalName = n.string.optional;
optionalName.normalize(undefined).data // undefined
Allows null values. Note: This changes the schema type.
const nullableName = n.string.nullable;
nullableName.normalize(null).data // null
Provides default for undefined
/null
.
const nameWithDefault = n.string.default("Anonymous");
nameWithDefault.normalize(null).data // "Anonymous"
nameWithDefault.normalize(undefined).data // "Anonymous"
nameWithDefault.normalize("Bob").data // "Bob"
Provides value when normalization has issues.
const safeName = n.string.fallback("Invalid");
safeName.normalize({ complex: "object" }).data // "Invalid"
Modify input before schema processing.
const trimmedString = n.string.preprocess((x) => {
return n.isString(x) ? x.trim() : x;
});
trimmedString.normalize(" hello ").data // "hello"
Convert after schema processing. Note: This changes the schema type.
const uppercaseName = n.string.transform(s => s.toUpperCase());
uppercaseName.normalize("alice").data // "ALICE"
Removes unknown properties (default behavior)
Keeps unknown properties.
const userSchema = n.object({
name: n.string,
age: n.number,
});
userSchema.passthrough.normalize({
name: "Bob",
age: 30,
extra: true
}).data // { name: "Bob", age: 30, extra: true }
Add more properties.
const extendedUser = userSchema.extend({
isActive: n.boolean
});
// You can also extend with another schema
const moreExtendedUser = extendedUser.extend(someOtherSchema);
Select specific properties.
const nameOnly = userSchema.pick(["name"]);
Exclude specific properties.
const withoutAge = userSchema.omit(["age"]);
Makes all record values optional.
const partialMetadata = n.record(n.string).partial;
Normul provides simple type-guard utilities for checking values. These are especially useful when validating incoming data in preprocess functions.
n.isObject
n.isArray
n.isString
n.isNumber
n.isBoolean
You can extract the normalized TypeScript type from any schema using n.Infer
:
const User = n.Infer<typeof userSchema>;
const apiResponseSchema = n.object({
success: n.boolean,
data: n.object({
users: n.array(
n.object({
id: n.number,
name: n.string,
lastLogin: n.string.transform(date => new Date(date)),
}),
),
pagination: n.object({
page: n.number,
perPage: n.number,
total: n.number,
})
}),
message: n.string.optional,
});
// Even with inconsistent API responses, you'll get predictable data
const normalizedResponse = apiResponseSchema.normalize(apiResponse).data;
const formSchema = n.object({
username: n.string.transform(s => s.trim()),
age: n.number,
newsletter: n.boolean,
});
// Process form data (which often comes as all strings)
const formData = {
username: " user123 ",
age: "25",
newsletter: "false",
};
const { data } = formSchema.normalize(formData);
// {
// username: "user123",
// age: 25,
// newsletter: false,
// }
const configSchema = n.object({
port: n.number.default(3000),
host: n.string.default("localhost"),
debug: n.boolean.default(false),
database: n.object({
url: n.string,
maxConnections: n.number.default(10),
}).nullable.default(null),
});
// Normalize partial config with sensible defaults
const { data: config } = configSchema.normalize({
port: "8080"
});
// {
// port: 8080,
// host: "localhost",
// debug: false,
// database: null,
// }
π
ββοΈ No Exceptions - Normul never throws during normalization. You always get back data
and issues
.
π§° Swiss Army Knife - Perfect for API responses, form data, configs, or anywhere with unpredictable data.
π Transformation-First - Unlike validation libraries that check data, Normul actively transforms it.
π§ Minimalist Philosophy - No complex validation rules or dependencies, just pure transformation.
Normul: Turn messy data into boring, predictable data.