Where this came from
This note comes from building compare-guard and related utility helpers. The real design work appeared when nested paths were missing, values were null, arrays entered the path, and runtime behavior stopped matching the neat TypeScript signature.
Open compare-guard on GitHubMy short checklist
- - Write the behavior matrix before designing the fluent API.
- - Make null, undefined, missing path, and array behavior explicit.
- - Avoid hidden coercion in comparison helpers.
- - Use tests as the real public contract.
Utility packages look simple because the first example is always simple. Read a nested value. Create an object from a path. Compare two fields. Then real input arrives: missing keys, arrays, null values, numeric strings, empty paths, objects with inherited properties, and users who expect the function to be both strict and forgiving.
Decide the contract before the API shape
The important design work is not whether the API is chainable. It is the behavior matrix. What should happen when a path segment is missing? Is null different from undefined? Does the function mutate the original object? Are arrays addressed by numeric indexes? Are dots inside key names supported? These choices should be documented and tested.
Avoid cleverness around missing data
For comparison helpers, missing data should not accidentally pass a rule. If a user checks whether profile.age is greater than 18, and profile is missing, the result should be predictable. I prefer explicit return values and named methods over hidden coercion. A utility should reduce uncertainty, not add a second language inside JavaScript.
- Missing object path should be tested.
- Null and undefined should be separate test cases.
- Array indexes should be covered.
- Mutation versus immutable return should be obvious.
- TypeScript types should guide usage but not pretend runtime data is safe.
Keep error messages useful
When a utility fails, the user needs to know which path or value caused the problem. A vague "invalid input" error wastes time. Even small libraries benefit from careful errors because they are often used deep inside data transformation code where the original input is not visible.
Tests are the product
For utility packages, tests are not decoration. They are the behavioral documentation. I like table-driven tests for path parsing, object creation, comparison edge cases, and weird but legal JavaScript values. The library can stay small, but the tests should be broad enough to protect the contract.
For example, a missing age, the string "29", and the number 29 should be separate test cases. The caller should be able to read the tests and predict the result.
import { compare, all } from "compare-guard";
const result = all([
compare(user, "profile.age").isNumber(),
compare(user, "profile.age").gte(18),
compare(user, "account.status").eq("active"),
]);