Personal helpers and convenience functions for TypeScript.
Links:
- Github:
carlwr/typescript-extra - npm:
@carlwr/typescript-extra - JSR:
@carlwr/typescript-extra
# one of:
npm install @carlwr/typescript-extra
pnpm dlx jsr add @carlwr/typescript-extra
# run checks and tests:
npm run qaIdentifiers live on the package root or on a subpath, e.g.:
import { memoized } from '@carlwr/typescript-extra'
import { andThen } from '@carlwr/typescript-extra/maybe-async'Node-only identifiers (not browser/bundler-safe) live under the /node subpath, e.g.:
import { rm_rf } from '@carlwr/typescript-extra/node'This README.md file, and any JSDoc documentation, is written entirely by me, Carl, a human developer. Agents are not allowed to touch the prose of these.
I-the-human hand-wrote almost all code, at least up to and including v0.12.2. For later versions, agentic tools may be used as a development aid; with myself as the reviewer and ultimate decision-maker. I am and will remain the sole author of this README.md file and any JSDoc.
function allUnique<T extends Primitive>(xs: T[]): booleanwhether the elements of xs are unique, in the === sense
O(n) (if the hash tables behave, and they should for primitives) (constants likely not too great)
function assertNever(_: never): neverMarker for exhaustiveness checks in discriminated unions.
function cached<T>(f: () => T): () => TCached, lazy evaluation.
If f throws, the result is not cached: the next call will evaluate f again.
This function can be thought of as the synchronous analogue to the promise-oriented memoized. Note though that the error-handling differs.
example:
const getCfg = cached(() => readFileSync('config.json', 'utf8'))
let c0 = getCfg() // reads the file, returns result
let c1 = getCfg() // returns cached resultfunction cachedUnary<K, V>(f: (k: K) => V): (k: K) => VCached, lazy evaluation of a single-argument function.
The cache is keyed by argument identity (SameValueZero, i.e. === except for on NaN).
If f throws, the result is not cached for that argument: the next call with the same argument will evaluate f again.
example:
const getText = cachedUnary((path: string) => readFileSync(path, 'utf8'))
getText('a.txt') // reads a.txt, returns its content
getText('a.txt') // returns cached a.txt content
getText('B.txt') // reads B.txt, returns its contentfunction drain<T>(xs: [T, ...T[]]): () => TStateful iterator yielding the elements of xs.
When the last element is reached, calls will continue to return that element indefinitely.
example:
const next = drain([1, 2, 3])
next() // => 1
next() // => 2
next() // => 3
next() // => 3
next() // => 3 (returns 3 forever)function escapeRegExp(s: string): stringescape regex meta-characters in s so the result matches s literally when used as a RegExp pattern
example:
new RegExp(escapeRegExp('a.b')) // matches "a.b" literally, not "a<any>b"function extract<T>(root: unknown, pred: (k: string, v: unknown) => T | undefined): T[]walk root recursively while collecting all Ts for which pred returns a defined result
example:
const root = {
A: 1,
b: 0,
c: {
99: [{A:2}, {e:"zero"}, null],
A: 3
}
}
function pred(k: string, v: any): number|undefined {
if (k === 'A' && typeof v === 'number') { return v }
return undefined
}
const result = extract<number>(root, pred)
// result === [1, 2, 3]function flatmapNonEmpty<T, U>(xs: readonly [T, T], fn: (x: T, i: number, arr: readonly [T, T]) => [U, ...U[]]): [U, ...U[]]Flatmap over a non-empty array with a function returning a non-empty arrays. Return the flattened result as a non-empty array.
The callback receives (x, i, arr) — same shape as Array.prototype.flatMap, with arr narrowed to non-empty.
function getMatch(re: RegExp, str: string): stringreturn the match of the regex, or throw if no match
function hasAtleastTwo<T>(xs: readonly T[]): xs is [T, T, ...T[]]function hasKey<T, K extends PropertyKey>(value: T, key: K): value is T & { [P in PropertyKey]: P extends keyof T ? Exclude<T[P], undefined> : unknown }whether the value is an object that has the key
in the true branch, the type of the passed argument is narrowed to include the knowledge that the key is present, without destroying any other knowledge about the type prior to the call
if K is a known key of T, the narrowed property type also has undefined excluded (the in check has proved the key is set)
function isDefined<T>(x: T | null | undefined): x is NonNullable<T>function isEmpty<T>(xs: readonly T[]): xs is []function isNonEmpty<T>(xs: readonly T[]): xs is [T, ...T[]]function isSingle<T>(xs: readonly T[]): xs is [T]function mapAsync<T, U>(xs: readonly T[], f: (x: T) => Promise<U>): Promise<U[]>Map an async function over xs.
function mapFilterAsync<T, U>(xs: readonly T[], f: (x: T) => Promise<U | null | undefined>): Promise<U[]>Map an async function over xs and keep only defined results.
function mapNonEmpty<T, U>(xs: readonly [T, T], fn: (x: T, i: number, arr: readonly [T, T]) => U): [U, ...U[]]Map over a non-empty array while preserving the type as non-empty.
The callback receives (x, i, arr) — same shape as Array.prototype.map, with arr narrowed to non-empty.
function memoized<T>(f: () => Promise<T>): () => Promise<T>Cached, lazy, single-flight evaluation of a promise.
A rejected promise is cached as well, i.e. no re-attempts (rejections are assumed to be permanent).
function memoizedRetry<T>(f: () => Promise<T>): () => Promise<T>Cached, lazy, single-flight evaluation of a promise.
A rejected promise is not cached: the next call will evaluate f again.
The retrying analogue of memoized.
function nonEmpty<T>(head: T, ...rest: T[]): [T, ...T[]]Construct a NonEmpty from a guaranteed head and zero or more tail elements.
Useful for declaring or building a [T,...T[]] without an explicit type annotation or cast.
example:
const xs = nonEmpty(1, 2, 3) // typed [number, ...number[]]function partitionAsync<A, B extends boolean>(items: readonly A[], pred: (a: A) => Promise<B>): Promise<readonly [A[], A[]]>Split items into those for which the async predicate resolves truthy and falsy.
function safeIndex<T>(xs: readonly [T, T], i: number): Treturn the ith element of xs, or throw if i is out of bounds
function trim(str: string): stringthe .trim() method as a function
trim(str) <=> str.trim()
function withoutFirstSubstring(first: string, str: string): stringremove a substring from the beginning of a string; throw if str does not start with first
example:
withoutFirstSubstring('e', 'ego') // => 'go'function rm_rf(path: string): Promise<void>remove a file or directory recursively; ignore errors
function andFinally<R, C = void>(run: () => R, cleanup: () => C & NotThenable<C>): R extends PromiseLike<unknown> ? Promise<Awaited<R>> : RRun the function run. Run the synchronous cleanup function once, regardless of whether run returned, threw or rejected.
If run returns a promise, cleanup is run when that promise has settled.
The return value of cleanup is always ignored.
What andFinally returns, and how it calls cleanup, is determined by whether run threw, and if it didn't, what it returned:
run |
-> | andFinally |
calls cleanup |
|---|---|---|---|
| returned a non-promise | -> | returns or throws | synchronously |
| threw | -> | throws | synchronously |
| returned a promise | -> | returns a native promise | asynchronously |
The outcome of andFinally, and what error is attached as cause, if any, is determined by whether run threw/rejected and whether cleanup threw:
run |
cleanup |
-> | outcome | cause set to |
|---|---|---|---|---|
| succeeded | returned | -> | run's value |
– |
| failed | returned | -> | run's error |
– |
| succeeded | threw | -> | cleanup's error |
– |
| failed | threw | -> | cleanup's error |
run's error, if unset |
If cleanup throws an error and run failed, cleanup's error object is mutated (can only matter if shared or reused).
In the documentation above, "promise" is used in the meaning of PromiseLike on the type level, and an object with a PromiseLike.then method at runtime. The two are typically the same but may disagree in rare cases.
function andThen<V, B>(value: V, f: (a: Awaited<V>) => B): V extends PromiseLike<unknown> ? Promise<Awaited<B>> : PromiseLike<unknown> extends V ? B | Promise<Awaited<B>> : BApply a function (f) to a value (value). value may or may not be a promise, and f may or may not return a promise.
If value is a promise, f receives the awaited value.
What andThen returns, and how it calls f, is determined by what value is:
value |
-> | andThen |
calls f |
|---|---|---|---|
| non-promise | -> | returns what f returned |
synchronously |
| fulfilling promise | -> | returns a native promise | asynchronously |
| rejecting promise | -> | returns a rejecting promise | never |
If f throws, then if value is
- a non-promise, the error propagates
- a promise, then the returned promise rejects
If value is a promise, a flattened (native) promise is returned.
If a .then getter of value throws, the returned promise will be a rejection.
In the documentation above, "promise" is used in the meaning of PromiseLike on the type level, and an object with a PromiseLike.then method at runtime. The two are typically the same but may disagree in rare cases.