Entity Tools provides a compact set of runtime utilities and type-level helpers for working with data models (Entities), sorting, shallow updates, and safe transcoding between values and lexicographically sortable strings.
npm i @karmaniverous/entity-tools
# or: pnpm add @karmaniverous/entity-tools
# or: yarn add @karmaniverous/entity-tools
Runtime utilities
Core types (entities and sorting)
Transcoding types
int → number).Type utilities
Full reference: see API docs linked at the top of this README.
Sorting
import { sort, type SortOrder } from '@karmaniverous/entity-tools';
type User = {
id: number;
name: string;
optional?: string | null;
data?: object;
};
const users: User[] = [
{ id: 2, name: 'Adam', optional: 'foo', data: { foo: 'bar' } },
{ id: 3, name: 'Bob', optional: 'bar', data: { bar: 'baz' } },
{ id: 1, name: 'Charlie', optional: null, data: { baz: 'qux' } },
{ id: 4, name: 'Adam' },
];
const order: SortOrder<User> = [
{ property: 'name' },
{ property: 'id', desc: true },
];
const result = sort(users, order);
// [
// { id: 4, name: 'Adam' },
// { id: 2, name: 'Adam', optional: 'foo', data: { foo: 'bar' } },
// { id: 3, name: 'Bob', optional: 'bar', data: { bar: 'baz' } },
// { id: 1, name: 'Charlie', optional: null, data: { baz: 'qux' } },
// ]
Shallow updates
import { updateRecord } from '@karmaniverous/entity-tools';
const original = {
id: 1,
name: 'Alice',
note: undefined as string | undefined,
};
const patch = {
name: 'Alicia',
note: null,
extra: undefined as string | undefined,
};
const updated = updateRecord(original, patch);
// { id: 1, name: 'Alicia' } // note and extra are removed (null/undefined stripped)
Conditional execution
import { conditionalize } from '@karmaniverous/entity-tools';
const debugLog = conditionalize(console.log, process.env.DEBUG);
debugLog?.('only logs when DEBUG is truthy');
Nil checks
import { isNil, type Nil } from '@karmaniverous/entity-tools';
function takesMaybe(v: unknown): v is Nil {
return isNil(v);
}
The builder is value-first and inference-first: pass a literal registry and get a strongly-typed Transcodes<…> back with strict agreement between encode and decode per key.
import { defineTranscodes } from '@karmaniverous/entity-tools';
import type {
Transcodes,
TranscodeRegistryFrom,
TranscodedType,
TranscodeName,
} from '@karmaniverous/entity-tools';
const mySpec = {
int: {
encode: (v: number) => v.toString(),
decode: (s: string) => Number(s),
},
boolean: {
encode: (v: boolean) => (v ? 't' : 'f'),
decode: (s: string) => s === 't',
},
} as const;
// Build the runtime registry (inference-first)
const myTranscodes = defineTranscodes(mySpec);
// ^? Transcodes<TranscodeRegistryFrom<typeof mySpec>>
// Extract the registry type without building:
type MyRegistry = TranscodeRegistryFrom<typeof mySpec>;
// Name and value helpers
type TInt = TranscodedType<MyRegistry, 'int'>; // number
type TNames = TranscodeName<MyRegistry>; // 'int' | 'boolean'
Agreement is enforced at compile time: if encode and decode disagree for any key, the spec becomes unsatisfiable and TypeScript will error.
import { defineTranscodes } from '@karmaniverous/entity-tools';
// @ts-expect-error encode/decode types do not agree
defineTranscodes({
bad: {
encode: (_v: unknown) => '',
// wrong decode return type on purpose:
decode: (_s: string) => 'oops',
},
} as const);
To improve DX, unsatisfied keys are branded in the agreement type so the compiler produces clearer error messages during development:
{ __error__: 'MissingEncode'; key: K }{ __error__: 'MissingDecode'; key: K }{ __error__: 'EncodeDecodeMismatch'; key: K; encodeParam: VK; decodeReturn: VK' }These branded shapes are used by the agreement type internally; the builder still rejects invalid specs, but the branded shapes help the compiler show more actionable messages when a spec is incorrect.
Example (type-only):
import type {
EncodeDecodeAgreement,
EncodeDecodeMismatchError,
} from '@karmaniverous/entity-tools';
// Produces a branded mismatch error for key 'bad'
type Mismatch = EncodeDecodeAgreement<{
bad: { encode: (v: number) => string; decode: (s: string) => boolean };
}>;
// Assignable to a branded error shape:
const sample: EncodeDecodeMismatchError<'bad', number, boolean> = {
__error__: 'EncodeDecodeMismatch',
key: 'bad',
encodeParam: 1 as number,
decodeReturn: true as boolean,
};
import { defaultTranscodes } from '@karmaniverous/entity-tools';
// Fixed-width integer (sign-prefixed, zero-padded; sorts lexicographically)
const enc = defaultTranscodes.int.encode(-123); // "n0000000000000123"
const dec = defaultTranscodes.int.decode(enc); // -123
// Fixed 6-decimal number
defaultTranscodes.fix6.encode(123.45); // "p0000000123.450000"
defaultTranscodes.fix6.decode('n0000000123.450000'); // -123.45
// Variable width number, boolean, string
defaultTranscodes.number.encode(42); // "42"
defaultTranscodes.boolean.decode('t'); // true
defaultTranscodes.string.encode('hello'); // "hello"
// BigInt (variable) and BigInt20 (fixed width up to 20 digits)
defaultTranscodes.bigint.encode(1234567890123456789n); // "1234567890123456789"
defaultTranscodes.bigint20.encode(-1234567890123456789n); // "n01234567890123456789"
import { encodePairs, decodePairs } from '@karmaniverous/entity-tools';
const pairs: Array<[string, string]> = [
['k1', 'v1'],
['k2', ''],
];
const enc = encodePairs(pairs); // 'k1#v1|k2#'
const dec = decodePairs(enc); // pairs
// custom delimiters
const enc2 = encodePairs(pairs, { pair: '~', kv: '::' }); // 'k1::v1~k2::'
const dec2 = decodePairs(enc2, { pair: '~', kv: '::' }); // pairs
import {
hashString,
enumerateShardSuffixes,
shardSuffixFromHash,
} from '@karmaniverous/entity-tools';
// hashString: deterministic 32-bit unsigned FNV-1a
const h = hashString('hello'); // number in [0, 2^32 - 1]
// enumerateShardSuffixes: all suffixes for radix/width
const hex2 = enumerateShardSuffixes(16, 2); // ['00', '01', ..., 'ff']
// shardSuffixFromHash: modulo shard space with padding
const s = shardSuffixFromHash(h, 16, 2); // e.g., 'a3'
import type {
MakeOptional,
MakeRequired,
MakeUpdatable,
WithRequiredAndNonNullable,
MutuallyExclusive,
PropertiesOfType,
PropertiesNotOfType,
ReplaceKey,
ReplaceKeys,
} from '@karmaniverous/entity-tools';
type User = { id: number; name: string; note?: string | null };
type OptionalNote = MakeOptional<User, 'note'>;
type RequiredId = WithRequiredAndNonNullable<User, 'id'>;
// Compile-time exclusivity check (returns true or an error-shape).
type Ex = MutuallyExclusive<['a', 'b' | 'c', 'd']>; // true
Built for you with ❤️ on Bali! Find more great tools & templates on my GitHub Profile.