@copper.teapot.ferret/typed-toolkit
v0.1.2
Published
Utility functions with types that don't lie.
Maintainers
Readme
typed-toolkit
Utility functions with types that don't lie.
A TypeScript utility library where every function's return type is exactly what your IDE should infer — no any, no casts, no surprises.
Install
npm install @copper.teapot.ferret/typed-toolkitWhat makes this different
Most utility libs return Record<string, unknown> where they could return the exact subtype. This library uses generics, mapped types, and conditional types so the compiler knows the answer before you do.
import { pick } from "@copper.teapot.ferret/typed-toolkit";
const user = { id: 1, name: "Alice", password: "secret" };
const safe = pick(user, ["id", "name"]);
// ^? { id: number; name: string } ← not Record<string, unknown>API
Object
pick<T, K>(obj, keys) → Pick<T, K>
import { pick } from "@copper.teapot.ferret/typed-toolkit";
pick({ a: 1, b: "x", c: true }, ["a", "b"]);
// { a: 1, b: "x" } typed as { a: number; b: string }Runtime contract: pick trusts the type system. If called from plain JavaScript with a key that doesn't exist on the object, it silently returns undefined for that key — same behavior as property access. TypeScript prevents this at compile time; at runtime, garbage in = garbage out.
omit<T, K>(obj, keys) → Omit<T, K>
import { omit } from "@copper.teapot.ferret/typed-toolkit";
omit({ a: 1, b: 2, c: 3 }, ["b", "c"]);
// { a: 1 } typed as { a: number }mapValues<K, V, U>(obj, fn) → Record<K, U>
import { mapValues } from "@copper.teapot.ferret/typed-toolkit";
mapValues({ a: 1, b: 2 }, (v) => v * 10);
// { a: 10, b: 20 } typed as Record<"a" | "b", number>Array
chunk<T>(arr, size) → T[][]
import { chunk } from "@copper.teapot.ferret/typed-toolkit";
chunk([1, 2, 3, 4, 5], 2);
// [[1, 2], [3, 4], [5]]unique<T>(arr) → T[]
import { unique } from "@copper.teapot.ferret/typed-toolkit";
unique([1, 2, 2, 3, 1]);
// [1, 2, 3]uniqueBy<T, K>(arr, fn) → T[]
import { uniqueBy } from "@copper.teapot.ferret/typed-toolkit";
uniqueBy([{ id: 1, v: "a" }, { id: 1, v: "b" }, { id: 2, v: "c" }], (x) => x.id);
// [{ id: 1, v: "a" }, { id: 2, v: "c" }]groupBy<T, K>(arr, fn) → Partial<Record<K, T[]>>
import { groupBy } from "@copper.teapot.ferret/typed-toolkit";
groupBy(["one", "two", "three"], (s) => s.length);
// { 3: ["one", "two"], 5: ["three"] }partition<T, U>(arr, predicate) → [U[], Exclude<T, U>[]]
With a type predicate, the two halves are narrowed automatically:
import { partition } from "@copper.teapot.ferret/typed-toolkit";
type Animal = { kind: "dog" } | { kind: "cat" };
const isDog = (a: Animal): a is { kind: "dog" } => a.kind === "dog";
const [dogs, cats] = partition(animals, isDog);
// ^? { kind: "dog" }[] ^? { kind: "cat" }[]Parsing (returns Result<T, E>, never throws)
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };parseJSON<T>(str) → Result<T, string>
import { parseJSON } from "@copper.teapot.ferret/typed-toolkit";
const result = parseJSON<{ name: string }>('{"name":"Alice"}');
if (result.ok) console.log(result.value.name); // "Alice"parseNumber(str) → Result<number, string>
import { parseNumber } from "@copper.teapot.ferret/typed-toolkit";
parseNumber("3.14"); // { ok: true, value: 3.14 }
parseNumber("abc"); // { ok: false, error: '"abc" is not a valid number' }parseDate(str) → Result<Date, string>
import { parseDate } from "@copper.teapot.ferret/typed-toolkit";
parseDate("2024-01-15"); // { ok: true, value: Date }
parseDate("nope"); // { ok: false, error: '"nope" is not a valid date' }Predicates
isNonNull<T>(value) → value is NonNullable<T>
import { isNonNull } from "@copper.teapot.ferret/typed-toolkit";
const nums = [1, null, 2, undefined, 3].filter(isNonNull);
// ^? number[]isDefined<T>(value) → value is T
import { isDefined } from "@copper.teapot.ferret/typed-toolkit";
const strs = ["a", undefined, "b"].filter(isDefined);
// ^? string[]String
capitalize<T extends string>(str) → Capitalize<T>
import { capitalize } from "@copper.teapot.ferret/typed-toolkit";
capitalize("hello");
// "Hello" typed as Capitalize<"hello">Architecture decisions
ADR-001 — Type tests alongside runtime tests. Every function has both Vitest runtime tests and expect-type type-level assertions. A function can pass all runtime tests and still have wrong types; expect-type catches that at compile time.
ADR-002 — tsup as bundler. Ships ESM + CJS from a single config. Consumers on both module systems get correct types via the exports field.
ADR-003 — Result<T, E> instead of exceptions. Fallible functions return a tagged union that forces the caller to handle the error branch. No silent swallows, no unchecked exceptions.
License
MIT
