A collection of TypeScript utilities that I use across my projects.
Run the following command to add utilful to your project.
# npm
npm install -D utilful
# pnpm
pnpm add -D utilful
# yarn
yarn add -D utilfulConverts MaybeArray<T> to Array<T>.
type MaybeArray<T> = T | T[]
declare function toArray<T>(array?: MaybeArray<T> | null | undefined): T[]Converts an array of objects to a comma-separated values (CSV) string that contains only the columns specified.
declare function createCSV<T extends Record<string, unknown>>(
data: T[],
columns: (keyof T)[],
options?: {
/** @default ',' */
delimiter?: string
/** @default true */
includeHeaders?: boolean
/** @default false */
quoteAll?: boolean
}
): stringExample:
const data = [
{ name: 'John', age: '30', city: 'New York' },
{ name: 'Jane', age: '25', city: 'Boston' }
]
const csv = createCSV(data, ['name', 'age'])
// name,age
// John,30
// Jane,25Parses a comma-separated values (CSV) string into an array of objects.
Note
The first row of the CSV string is used as the header row.
type CSVRow<T extends string = string> = Record<T, string>
declare function parseCSV<Header extends string>(
csv?: string | null | undefined,
options?: {
/** @default ',' */
delimiter?: string
/** @default true */
trimValues?: boolean
}
): CSVRow<Header>[]Example:
const csv = `name,age
John,30
Jane,25`
const data = parseCSV<'name' | 'age'>(csv) // [{ name: 'John', age: '30' }, { name: 'Jane', age: '25' }]Recursively assign default properties. Simplified version based on unjs/defu.
Recursively assigns missing properties from defaults to the source object. The source object takes precedence over defaults.
Key Features:
- Null/undefined handling:
nullandundefinedvalues in source are replaced with defaults - Array concatenation: Arrays are concatenated (source + defaults)
- Deep merging: Nested objects are recursively merged
- Type safety: Preserves TypeScript types
- Prototype pollution protection: Ignores
__proto__andconstructorkeys
type PlainObject = Record<PropertyKey, any>
declare function defu<T extends PlainObject>(
source: T,
...defaults: PlainObject[]
): TExample:
import { defu } from 'utilful'
const result = defu(
{ a: 1, b: { x: 1 } },
{ a: 2, b: { y: 2 }, c: 3 }
)
// Result: { a: 1, b: { x: 1, y: 2 }, c: 3 }Array concatenation example:
const result = defu(
{ items: ['a', 'b'] },
{ items: ['c', 'd'] }
)
// Result: { items: ['a', 'b', 'c', 'd'] }Null/undefined handling:
const result = defu(
{ name: null, age: undefined },
{ name: 'John', age: 30, city: 'NYC' }
)
// Result: { name: 'John', age: 30, city: 'NYC' }Creates a custom defu function with a custom merger.
type DefuMerger<T extends PlainObject = PlainObject> = (
target: T,
key: PropertyKey,
value: any,
namespace: string,
) => boolean | void
declare function createDefu(merger?: DefuMerger): DefuFnExample:
import { createDefu } from 'utilful'
// Custom merger that adds numbers instead of replacing them
const addNumbers = createDefu((obj, key, val) => {
if (typeof val === 'number' && typeof obj[key] === 'number') {
obj[key] += val
return true // Indicates the merger handled this property
}
})
const result = addNumbers({ cost: 15 }, { cost: 10 })
// Result: { cost: 25 }Tiny functional event emitter / pubsub, based on mitt.
Example:
import { createEmitter } from 'utilful'
// eslint-disable-next-line ts/consistent-type-definitions
type Events = {
foo: { a: string }
}
const emitter = createEmitter<Events>()
// Listen to an event
emitter.on('foo', e => console.log('foo', e))
// Listen to all events
emitter.on('*', (type, e) => console.log(type, e))
// Fire an event
emitter.emit('foo', { a: 'b' })
// Clearing all events
emitter.events.clear()
// Working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // Listen
emitter.off('foo', onFoo) // UnlistenType-safe wrapper around JSON.stringify.
Falls back to the original value if the JSON serialization fails or the value is not a string.
declare function tryParseJSON<T = unknown>(value: unknown): TClones the given JSON value.
Note
The value must not contain circular references as JSON does not support them. It also must contain JSON serializable values.
declare function cloneJSON<T>(value: T): TInterop helper for default exports.
declare function interopDefault<T>(m: T | Promise<T>): Promise<T extends {
default: infer U
} ? U : T>Example:
import { interopDefault } from 'utilful'
async function loadModule() {
const mod = await interopDefault(import('./module.js'))
}A simple general purpose memoizer utility.
- Lazily computes a value when accessed
- Auto-caches the result by overwriting the getter
Useful for deferring initialization or expensive operations. Unlike a simple getter, there is no runtime overhead after the first invokation, since the getter itself is overwritten with the memoized value.
declare function memoize<T>(getter: () => T): { value: T }Example:
const myValue = lazy(() => 'Hello, World!')
console.log(myValue.value) // Computes value, overwrites getter
console.log(myValue.value) // Returns cached value
console.log(myValue.value) // Returns cached valueStrictly typed Object.keys.
declare function objectKeys<T extends Record<any, any>>(obj: T): Array<`${keyof T & (string | number | boolean | null | undefined)}`>Strictly typed Object.entries.
declare function objectEntries<T extends Record<any, any>>(obj: T): Array<[keyof T, T[keyof T]]>Deeply applies a callback to every key-value pair in the given object, as well as nested objects and arrays.
declare function deepApply<T extends Record<any, any>>(data: T, callback: (item: T, key: keyof T, value: T[keyof T]) => void): voidRemoves the leading slash from the given path if it has one.
declare function withoutLeadingSlash(path?: string): stringAdds a leading slash to the given path if it does not already have one.
declare function withLeadingSlash(path?: string): stringRemoves the trailing slash from the given path if it has one.
declare function withoutTrailingSlash(path?: string): stringAdds a trailing slash to the given path if it does not already have one.
declare function withTrailingSlash(path?: string): stringJoins the given URL path segments, ensuring that there is only one slash between them.
declare function joinURL(...paths: (string | undefined)[]): stringAdds the base path to the input path, if it is not already present.
declare function withBase(input?: string, base?: string): stringRemoves the base path from the input path, if it is present.
declare function withoutBase(input?: string, base?: string): stringReturns the pathname of the given path, which is the path without the query string.
declare function getPathname(path?: string): stringReturns the URL with the given query parameters. If a query parameter is undefined, it is omitted.
declare function withQuery(input: string, query?: QueryObject): stringExample:
import { withQuery } from 'utilful'
const url = withQuery('https://example.com', {
foo: 'bar',
// This key is omitted
baz: undefined,
// Object values are stringified
baz: { qux: 'quux' }
})The Result type that represents either success (Ok) or failure (Err). It helps to handle errors in a more explicit and type-safe way, without relying on exceptions.
A common use case for Result is error handling in functions that might fail. Here's an example of a function that divides two numbers and returns a Result:
import { err, ok } from 'utilful'
function divide(a: number, b: number) {
if (b === 0) {
return err('Division by zero')
}
return ok(a / b)
}
const result = divide(10, 2)
if (result.ok)
console.log('Result:', result.value)
else
console.error('Error:', result.error)The Result type represents either success (Ok) or failure (Err).
Type Definition:
type Result<T, E> = Ok<T> | Err<E>The Ok type wraps a successful value.
Example:
const result = new Ok(42)The Err type wraps an error value.
Example:
const result = new Err('Something went wrong')Shorthand function to create an Ok result. Use it to wrap a successful value.
Type Definition:
declare function ok<T>(value: T): Ok<T>Shorthand function to create an Err result. Use it to wrap an error value.
Type Definition:
declare function err<E extends string = string>(err: E): Err<E>
declare function err<E = unknown>(err: E): Err<E>Wraps a function that might throw an error and returns a Result with the result of the function.
Type Definition:
declare function toResult<T, E = unknown>(fn: () => T): Result<T, E>
declare function toResult<T, E = unknown>(promise: Promise<T>): Promise<Result<T, E>>Unwraps a Result, Ok, or Err value and returns the value or error in an object. If the result is an Ok, the object contains the value and an undefined error. If the result is an Err, the object contains an undefined value and the error.
Example:
const result = toResult(() => JSON.parse('{"foo":"bar"}'))
const { value, error } = unwrapResult(result)Type Definition:
declare function unwrapResult<T>(result: Ok<T>): { value: T, error: undefined }
declare function unwrapResult<E>(result: Err<E>): { value: undefined, error: E }
declare function unwrapResult<T, E>(result: Result<T, E>): { value: T, error: undefined } | { value: undefined, error: E }A simpler alternative to toResult + unwrapResult. It executes a function that might throw an error and directly returns the result in a ResultData format. Works with both synchronous functions and promises.
Example:
import { tryCatch } from 'utilful'
// Synchronous usage
const { value, error } = tryCatch(() => JSON.parse('{"foo":"bar"}'))
// Asynchronous usage
const { value, error } = await tryCatch(fetch('https://api.example.com/data').then(r => r.json()))Type Definition:
declare function tryCatch<T, E = unknown>(fn: () => T): { value: T, error: undefined } | { value: undefined, error: E }
declare function tryCatch<T, E = unknown>(promise: Promise<T>): Promise<{ value: T, error: undefined } | { value: undefined, error: E }>Simple template engine to replace variables in a string.
declare function template(
str: string,
variables: Record<string | number, any>,
fallback?: string | ((key: string) => string)
): stringExample:
import { template } from 'utilful'
const str = 'Hello, {name}!'
const variables = { name: 'world' }
console.log(template(str, variables)) // Hello, world!Generates a random string. The function is ported from nanoid. You can specify the size of the string and the dictionary of characters to use.
declare function generateRandomId(size?: number, dict?: string): stringMIT License © 2024-PRESENT Johann Schopplich