@kung-fu/proto is a small utility library that wraps JavaScript primitives in fluent helpers. The package exports five wrapper factories — s(), a(), n(), obj(), helem() — plus the standalone range() helper.
Each wrapper holds a mutable internal value, so most methods modify state in place and also return the same wrapper. The chained style is canonical, but storing a reference and calling methods imperatively works identically. The schema interfaces (IString, IArray<T>, etc.) are internal — rely on TypeScript inference at the call site rather than importing them.
---// install
npm install @kung-fu/proto
---// import
import { s, a, n, obj, helem, range } from '@kung-fu/proto';
---// strings // s(value: string)
Wraps a string. Chainable transforms mutate the wrapper’s value and return this; predicates and getters return primitives.
| method | returns | description |
lower() / upper() | wrapper | case conversion (lower uses toLocaleLowerCase, upper uses toUpperCase) |
trim() / ltrim() / rtrim() | wrapper | regex-based whitespace trimming |
normalize() | wrapper | collapse runs of whitespace into single spaces, trim outer space |
capFirst() | wrapper | uppercase the first letter; if the string starts with (, [, ", or ', uppercase the next character too |
capWords() | wrapper | apply capFirst to each space-separated token |
truncateWords(n) | wrapper | keep only the first n whitespace-delimited words |
truncateWordsWithHtml(n) | wrapper | truncate then re-emit closing tags for any opener still in scope |
stripHtml() | wrapper | strip tags, <script> + <style> blocks, HTML comments, and decode / & |
escapeHtml() | wrapper | escapes &, <, >, and "; the & rule skips already-encoded entities so the call is idempotent |
slugify(lower?) | wrapper | normalize, then replace any non-[a-z0-9] with -; lowercases by default (lower defaults to true) |
startsWith(part, pos?) / endsWith(part, pos?) | boolean | substring tests; pos follows native String.prototype semantics (start offset for startsWith, length limit for endsWith) |
contains(val) | boolean | indexOf(val) !== -1 |
isNullOrEmpty() | boolean | true for null, undefined, or whitespace-only |
toBool() | boolean | case-insensitive parse: true, 1, y, t, on are true; everything else is false |
getValueByKey(k) | string | parse a k1:v1;k2:v2 blob and return the value for k ('' if missing) |
setValueByKey(k, v) | new wrapper | return a fresh wrapper with the given key’s value updated |
toString() | string | unwrap to a native string |
Slugify a title (the chain mutates and the unwrap returns):
s(' Hello, World! ').slugify().toString();
// "hello-world"
capFirst handles leading punctuation:
s('"hello there"').capFirst().toString();
// '"Hello there"'
s('(parens stay)').capFirst().toString();
// "(Parens stay)"
Strip and truncate user-submitted HTML for a teaser:
const teaser = s(post.body)
.stripHtml()
.normalize()
.truncateWords(40)
.toString();
Truncate while keeping HTML structure:
s('<p>Lorem <em>ipsum dolor</em> sit amet</p>')
.truncateWordsWithHtml(3)
.toString();
// "<p>Lorem <em>ipsum dolor</em></p>"
Truthy-string parser — useful for query-string flags:
s('on').toBool(); // true
s('Yes').toBool(); // false (only 'y' / 't' single letters match)
s('1').toBool(); // true
s('').toBool(); // false
Parse and update a semicolon-delimited key/value blob:
s('user:alice;role:admin;tz:utc').getValueByKey('role');
// "admin"
s('user:alice;role:admin').setValueByKey('role', 'guest').toString();
// "user:alice;role:guest"
---// arrays // a<T>(value: T[])
Wraps an array. Generic over T at the wrapper level, but note that each’s callback types its element parameter as any.
| method | returns | description |
empty() | wrapper | set the array to [] |
isEmpty() | boolean | length === 0 |
each(cb) | void | iterate; cb(index: number, item: any) |
remove(item) | wrapper | remove the first occurrence (allocates a new array internally; the wrapped reference is replaced) |
contains(p, strict) | boolean | strict=true uses === across any element type; strict=false looks for p as a substring within string elements only |
indexOfPartial(p) | number | first index where a string element contains p (-1 if none) |
toObjectArray(name) | wrapper | wrap each primitive (string / number / boolean) element as { [name]: value }; non-primitives pass through unchanged. Throws if name is null or undefined |
toArray() | T[] | unwrap to a native array |
Iterate with the index-first callback:
a(['alpha', 'beta', 'gamma']).each((i, item) => {
console.log(`${i}: ${item}`);
});
Loose substring contains versus strict equality:
const tags = ['javascript', 'typescript', 'rust'];
a(tags).contains('script', false); // true (substring match)
a(tags).contains('script', true); // false (no exact element)
a(tags).contains('rust', true); // true
a(tags).indexOfPartial('type'); // 1
Reshape a primitive list for an API that wants [{ id }, …]:
a([1, 2, 3]).toObjectArray('id').toArray();
// [{ id: 1 }, { id: 2 }, { id: 3 }]
---// numbers // n(value: number)
| method | returns | description |
toBool() | boolean | false when the value is exactly 0; true for everything else (including negative numbers and NaN) |
random(min, max) | wrapper | set the wrapper’s value to a random integer in [min, max] (inclusive). Throws if either argument is NaN; numeric strings are accepted because both arguments are coerced via parseInt |
toNumber() | number | unwrap |
The wrapper is reusable — random overwrites the held value, so chaining draws new numbers from one wrapper:
const rng = n(0);
rng.random(1, 6).toNumber(); // e.g. 4
rng.random(1, 6).toNumber(); // e.g. 1
---// objects // obj(value: any)
Tiny helper for ad-hoc shallow merging. extend mutates the wrapped object in place using only its own enumerable properties (it filters via hasOwnProperty).
const settings = { theme: 'dark', size: 14 };
obj(settings).extend({ size: 16, lineHeight: 1.5 }).toObject();
// { theme: 'dark', size: 16, lineHeight: 1.5 }
// settings is the same reference, mutated in place.
---// html form elements // helem(elem)
Smart wrapper around form elements. val() dispatches on nodeName + the input’s type attribute and handles each variant the way you usually want, so calling code doesn’t have to branch.
| element | get behavior | set behavior |
<textarea> | returns .value; falls back to innerText, then innerHTML (returns '' if all empty) | sets innerHTML, innerText, and .value (errors swallowed and logged) |
<input type="checkbox"> | returns .checked (boolean) | truthy val → checked, falsy → unchecked |
<input type="radio"> | returns the value of whichever radio in the same name-group is checked, or '' | queries every radio with the same name and checks the one whose value matches |
<input type="date"> | returns .value | strips a trailing T... ISO time component; converts MM/DD/YYYY to YYYY-MM-DD before assigning |
<input type="time"> | returns .value | trims HH:MM:SS down to HH:MM |
<input type="file"> | returns .value (the path stub) | no-op (browsers disallow programmatic file-input writes) |
any other <input> | returns .value | assigns .value |
<select> single | value of the selected option, or '' if none | finds the option whose value matches and selects it |
<select multiple> | semicolon-delimited string of selected values | same single-select selection (matches one option only) |
clean() normalizes line breaks in the element’s value to \r\n — useful before posting a textarea to a backend that expects DOS-style EOLs.
toHTMLElement() returns the underlying element if you need to drop back to vanilla DOM.
const input = document.querySelector('#email') as HTMLInputElement;
const current = helem(input).val(); // string | number | boolean
helem(input).val('user@example.com');
// Set a date input from any common format:
helem(dateInput).val('2026-05-06T12:00:00'); // becomes 2026-05-06
helem(dateInput).val('05/06/2026'); // becomes 2026-05-06
// Pick a radio in a named group:
// <input type="radio" name="plan" value="pro">
// <input type="radio" name="plan" value="free">
helem(anyRadio).val('pro');
// Read a multi-select as one string:
helem(multi).val(); // "alpha;beta;gamma"
---// range // range(count, start?)
Returns a fresh native number[] of count sequential integers starting at start (default 1). Despite the parameter name in the source TSDoc, the first argument is the array length, not an upper bound.
range(5); // [1, 2, 3, 4, 5]
range(5, 10); // [10, 11, 12, 13, 14]
range(0); // []
---// source