npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gmana/utils

v1.6.0

Published

Utility functions for React and TypeScript projects.

Downloads

885

Readme

@gmana/utils

A lightweight, dependency-free collection of utility functions for TypeScript and React projects.

Installation

npm install @gmana/utils
# or
pnpm add @gmana/utils
# or
bun add @gmana/utils

API Reference

Array

chunk<T>(array: T[], size: number): T[][]

Splits an array into chunks of the specified size.

chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]

compact<T>(array: (T | Falsy)[]): T[]

Removes all falsy values (null, undefined, false, 0, "") from an array.

compact([1, null, 2, undefined, 3]) // [1, 2, 3]

countBy<T, K>(array: T[], keyFn: (item: T) => K): Record<K, number>

Counts occurrences of each key returned by keyFn.

countBy(['a', 'b', 'a'], x => x) // { a: 2, b: 1 }

difference<T>(array1: T[], array2: T[]): T[]

Returns elements in array1 that are not in array2.

difference([1, 2, 3], [2]) // [1, 3]

drop<T>(array: T[], n: number): T[]

Removes the first n elements from an array.

drop([1, 2, 3, 4], 2) // [3, 4]

dropRight<T>(array: T[], n: number): T[]

Removes the last n elements from an array.

dropRight([1, 2, 3, 4], 2) // [1, 2]

flattenArray<T>(array: (T | T[])[]): T[]

Flattens one level of nesting.

flattenArray([1, [2, 3], [4]]) // [1, 2, 3, 4]

flattenDeepArray<T>(array): T[]

Deeply flattens all levels of nesting.

flattenDeepArray([1, [2, [3, [4]]]]) // [1, 2, 3, 4]

groupBy<T, K>(array: T[], key: (item: T) => K): Record<K, T[]>

Groups array elements by the key returned by key.

groupBy([{ type: 'a' }, { type: 'b' }, { type: 'a' }], x => x.type)
// { a: [{ type: 'a' }, { type: 'a' }], b: [{ type: 'b' }] }

groupConsecutive<T, K>(array: T[], keyFn: (item: T) => K): T[][]

Groups consecutive elements that share the same key into sub-arrays.

groupConsecutive([1, 1, 2, 2, 1], x => x)
// [[1, 1], [2, 2], [1]]

intersection<T>(array1: T[], array2: T[]): T[]

Returns elements common to both arrays.

intersection([1, 2, 3], [2, 3, 4]) // [2, 3]

maxBy<T>(array: T[], selector: (item: T) => number): T | undefined

Returns the element with the highest value per selector.

maxBy([{ n: 1 }, { n: 3 }, { n: 2 }], x => x.n) // { n: 3 }

meanBy<T>(array: T[], selector: (item: T) => number): number

Returns the average of values returned by selector.

meanBy([{ n: 1 }, { n: 2 }, { n: 3 }], x => x.n) // 2

minBy<T>(array: T[], selector: (item: T) => number): T | undefined

Returns the element with the lowest value per selector.

minBy([{ n: 1 }, { n: 3 }, { n: 2 }], x => x.n) // { n: 1 }

partition<T>(array: T[], predicate: (item: T, index: number) => boolean): [T[], T[]]

Splits an array into two groups: elements that satisfy the predicate and those that don't.

partition([1, 2, 3, 4], x => x % 2 === 0) // [[2, 4], [1, 3]]

sample<T>(array: T[], n?: number): T[]

Returns n random elements from the array (default 1).

sample([1, 2, 3, 4, 5], 2) // e.g. [3, 5]

shuffle<T>(array: T[]): T[]

Returns a new array with elements shuffled using the Fisher-Yates algorithm.

shuffle([1, 2, 3, 4]) // e.g. [3, 1, 4, 2]

sortBy<T>(array: T[], ...selectors: ((item: T) => unknown)[]): T[]

Sorts by one or more selector functions.

sortBy(
  [{ last: 'B', first: 'Z' }, { last: 'A', first: 'A' }, { last: 'A', first: 'B' }],
  u => u.last,
  u => u.first
)
// [{ last: 'A', first: 'A' }, { last: 'A', first: 'B' }, { last: 'B', first: 'Z' }]

sumBy<T>(array: T[], selector: (item: T) => number): number

Sums the values returned by selector.

sumBy([{ price: 10 }, { price: 20 }, { price: 5 }], x => x.price) // 35

symmetricDifference<T>(array1: T[], array2: T[]): T[]

Returns elements present in either array but not both.

symmetricDifference([1, 2, 3], [2, 3, 4]) // [1, 4]

take<T>(array: T[], n: number): T[]

Takes the first n elements.

take([1, 2, 3, 4], 2) // [1, 2]

takeRight<T>(array: T[], n: number): T[]

Takes the last n elements.

takeRight([1, 2, 3, 4], 2) // [3, 4]

unique<T>(array: T[]): T[]

Removes duplicate values.

unique([1, 2, 2, 3, 3]) // [1, 2, 3]

uniqueBy<T, K>(array: T[], keyFn: (item: T) => K): T[]

Removes duplicates based on the key returned by keyFn.

uniqueBy([{ id: 1, v: 'a' }, { id: 1, v: 'b' }, { id: 2, v: 'c' }], x => x.id)
// [{ id: 1, v: 'a' }, { id: 2, v: 'c' }]

zip<T>(...arrays: T): Array<[...]>

Zips multiple arrays together into an array of tuples.

zip([1, 2, 3], ['a', 'b', 'c']) // [[1, 'a'], [2, 'b'], [3, 'c']]

Object

compactObject<T>(input: T, options?: CompactOptions): Partial<T>

Removes empty/falsy values from an object.

interface CompactOptions {
  compactArrays?: boolean
  removeEmptyArrays?: boolean
  isEmpty?: (value: unknown) => boolean
}
compactObject({ a: 1, b: null, c: '' })                      // { a: 1 }
compactObject({ a: [1, null, 2] }, { compactArrays: true })  // { a: [1, 2] }

pick<T, K extends keyof T>(names: K[], obj: T): Pick<T, K>

Picks specified keys from an object. Also available in curried form.

pick(['a', 'b'], { a: 1, b: 2, c: 3 }) // { a: 1, b: 2 }
pick(['a', 'b'])({ a: 1, b: 2, c: 3 }) // { a: 1, b: 2 }

flatten(obj: Record<string, unknown>, separator?: string): Record<string, unknown>

Flattens a nested object into a single level using the given separator (default ".").

flatten({ a: { b: { c: 1 } } })          // { 'a.b.c': 1 }
flatten({ a: { b: 1 } }, '_')            // { 'a_b': 1 }

String

getInitialLetter(fullName?: string | null, fallback?: string): string

Extracts 1–2 letter initials from a name.

getInitialLetter('Sun Sreng')       // 'SS'
getInitialLetter('Sun')             // 'S'
getInitialLetter(null, 'N/A')       // 'N/A'

makeTitle(base: string, site: string, params: TemplateParams): string

Builds a page title from a base, site name, and optional template.

makeTitle('Jobs in Tech', 'Acme', {})
// 'Jobs in Tech | Acme'

makeTitle('Jobs in Tech', 'Acme', { template: '%s - Powered by Acme' })
// 'Jobs in Tech - Powered by Acme'

makeTitle('Jobs in Tech', 'Acme', { template: (title, site) => `${title} :: ${site}` })
// 'Jobs in Tech :: Acme'

makeTitle('Acme OG Preview', 'Acme', { disableSuffix: true })
// 'Acme OG Preview'

toCase(input: string, type: CaseType): string

Converts a string to the specified case. Supported types: lowercase, uppercase, sentence, title, snake, kebab, dot, constant, pascal, camel.

toCase('hello world', 'pascal')    // 'HelloWorld'
toCase('hello world', 'camel')     // 'helloWorld'
toCase('helloWorld', 'snake')      // 'hello_world'
toCase('helloWorld', 'kebab')      // 'hello-world'
toCase('helloWorld', 'dot')        // 'hello.world'
toCase('helloWorld', 'constant')   // 'HELLO_WORLD'
toCase('helloWorld', 'title')      // 'Hello World'
toCase('helloWorld', 'sentence')   // 'Hello world'

extendCases(custom: Record<string, CaseDefinition>): void

Registers custom case transformers for use with toCase.

extendCases({
  'pipe': {
    steps: [tokenize, normalizeLower],
    format: (ctx) => ctx.words.join('|'),
  },
})
toCase('hello world', 'pipe') // 'hello|world'

truncateText(text?: string, options?: TruncateTextOptions): string | undefined

Truncates text with an ellipsis.

interface TruncateTextOptions {
  maxLength?: number
  ellipsis?: string
  preserveWords?: boolean
  returnUndefinedIfEmpty?: boolean
}
truncateText('Hello world', { maxLength: 7 })                          // 'Hell...'
truncateText('Hello world', { maxLength: 7, preserveWords: true })     // 'Hello...'
truncateText('Hello world', { maxLength: 7, ellipsis: '…' })           // 'Hell…'
truncateText('', { returnUndefinedIfEmpty: true })                     // undefined

Number

formatBytes(bytes: number, options?: FormatBytesOptions): string

Formats a byte count to a human-readable string.

formatBytes(1536)                           // '1.50 KB'
formatBytes(1048576)                        // '1.00 MB'
formatBytes(1500, { precision: 0 })         // '1 KB'
formatBytes(1500, { base: 1000 })           // '1.50 KB'

toBytes(input: ByteInput, options?: Partial<ByteConvertOptions>): number

Converts a byte string to a raw number.

toBytes('2.5gb')                            // 2684354560
toBytes('1kb')                              // 1024
toBytes('1kb', { base: 1000 })             // 1000

createByteConverter(defaultOptions?): (input, overrides?) => number

Creates a pre-configured byte converter function.

const convert = createByteConverter({ base: 1000 })
convert('1kb')   // 1000
convert('1mb')   // 1000000

formatNumber(value?: number | null, decimalPlaces?: number): string

Formats a number with suffix notation (K, M, B, T).

formatNumber(1500)           // '1.5K'
formatNumber(2000000)        // '2M'
formatNumber(1234567, 2)     // '1.23M'
formatNumber(-1500)          // '-1.5K'
formatNumber(0)              // '0'

formatCurrency(options): string

Formats a number as a localized currency string.

formatCurrency({ amount: 1234.5, currencyCode: 'USD' })                      // '$1,234.50'
formatCurrency({ amount: 1234.5, currencyCode: 'EUR', locale: 'de-DE' })     // '1.234,50 €'
formatCurrency({ amount: 1000, currencyCode: 'KHR' })                        // '៛1,000'

numberToWord(n: number): string

Converts a number to English words.

numberToWord(42)          // 'Forty Two'
numberToWord(1000)        // 'One Thousand'
numberToWord(1234567.99)  // 'One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven Point Eight Nine'

numberToWordKm(value: number): string

Converts a number to Khmer words.

numberToWordKm(5)           // 'ប្រាំ'
numberToWordKm(1000)        // 'មួយពាន់'
numberToWordKm(1234567.89)  // 'មួយលាន ពីរសែន បីម៉ឺន បួនពាន់ ប្រាំរយ ហុកសិបប្រាំពីរ ក្បៀស ប្រាំបី ប្រាំបួន'

toASCII(s: string): string

Converts Khmer numerals to ASCII digits.

toASCII('០១២៣') // '0123'

toKhmer(s: string): string

Converts ASCII digits to Khmer numerals.

toKhmer('0123') // '០១២៣'

Date / Time

formatTime(seconds: number, options?: FormatTimeOptions): string

Formats a duration in seconds to a human-readable string.

interface FormatTimeOptions {
  format?: 'digital' | 'long' | 'short' | 'compact'
  alwaysShowHours?: boolean
  roundingMode?: 'floor' | 'ceil' | 'round'
  padMinutes?: boolean
  separator?: string
}
formatTime(3661, { format: 'digital' })  // '1:01:01'
formatTime(3661, { format: 'long' })     // '1 hour 1 minute 1 second'
formatTime(3661, { format: 'short' })    // '1h 1m 1s'
formatTime(3661, { format: 'compact' })  // '1h'

parseTime(timeString: string, separator?: string): number

Parses a formatted time string back to seconds.

parseTime('1:01:01')   // 3661
parseTime('0:30')      // 1800

toIso(value?: Date | string): string | undefined

Converts a Date or date string to ISO 8601 format.

toIso(new Date('2024-01-15'))   // '2024-01-15T00:00:00.000Z'
toIso('2024-01-15')             // '2024-01-15T00:00:00.000Z'
toIso()                         // undefined

URL / Path

absoluteUrl(path?, options?): string

Creates an absolute URL from a path, with optional query params and fragment.

absoluteUrl('/users', { query: { page: '2' }, baseUrl: 'https://example.com' })
// 'https://example.com/users?page=2'

absoluteUrl('/about', { fragment: 'team', baseUrl: 'https://example.com' })
// 'https://example.com/about#team'

joinPaths(segments: (string | null | undefined)[]): string

Joins URL path segments, handling leading/trailing slashes.

joinPaths(['api', 'users', '123'])      // 'api/users/123'
joinPaths(['/api/', '/users/', null])   // 'api/users'

isUrl(url: string | URL): boolean

Validates a URL, requiring http or https scheme.

isUrl('https://example.com')   // true
isUrl('ftp://example.com')     // false
isUrl('not-a-url')             // false

isValidUrl(url: string): boolean

Validates a URL string (any scheme).

isValidUrl('https://example.com')   // true
isValidUrl('ftp://example.com')     // true
isValidUrl('not-a-url')             // false

Type Checking

isArray(value: unknown): boolean

isArray([1, 2, 3])   // true
isArray('hello')     // false

isBoolean(value: unknown): boolean

isBoolean(true)    // true
isBoolean(1)       // false

isEmpty(value: unknown): boolean

Returns true for null, undefined, {}, [], and "".

isEmpty(null)   // true
isEmpty([])     // true
isEmpty({})     // true
isEmpty(0)      // false

isFunction(value: unknown): boolean

isFunction(() => {})   // true
isFunction('fn')       // false

isNumber(value: unknown): boolean

Excludes NaN.

isNumber(42)     // true
isNumber(NaN)    // false

isObject(value: unknown): boolean

Returns true for non-null objects (excludes arrays).

isObject({ a: 1 })   // true
isObject(null)       // false

isString(value: unknown): boolean

isString('hello')   // true
isString(42)        // false

isSymbol(value: unknown): boolean

isSymbol(Symbol('s'))   // true
isSymbol('s')           // false

isUndef(value: unknown): boolean

isUndef(undefined)   // true
isUndef(null)        // false

isValidJsonString(str: string): boolean

isValidJsonString('{"a":1}')   // true
isValidJsonString('{bad}')     // false

isValidComponentName(name: string, options?): ComponentValidationResult

Validates a kebab-case component name.

isValidComponentName('my-button')    // { valid: true, errors: [] }
isValidComponentName('MyButton')     // { valid: false, errors: ['...'] }

isDev

true when NODE_ENV is "development" or "test".

if (isDev) console.log('debug info')

isNavigator

true when the navigator global is available (browser environment).

if (isNavigator) console.log(navigator.userAgent)

CSS / Styling

clsx(...args: ClassValue[]): string

Conditionally joins class names.

clsx('foo', { bar: true, baz: false })         // 'foo bar'
clsx('a', undefined, null, 'b')                // 'a b'
clsx(['x', { y: true }])                       // 'x y'

cn(...inputs: ClassValue[]): string

Combines clsx with tailwind-merge to merge Tailwind classes without conflicts.

cn('px-2 py-1', 'px-4')                        // 'py-1 px-4'
cn('text-red-500', { 'text-blue-500': true })   // 'text-blue-500'

Token / Auth

getTokenExpClaim(token: string): number | null

Extracts the exp claim (Unix timestamp) from a JWT without verifying the signature.

const exp = getTokenExpClaim('eyJ...')   // e.g. 1713000000

isTokenExpired(token: string, offsetSeconds?: number): boolean

Returns true if the JWT is expired, with an optional clock skew offset.

isTokenExpired('eyJ...')          // false (if still valid)
isTokenExpired('eyJ...', 300)     // true if expiring within 5 minutes

OS Detection

getOS(userAgent: string): { type: OS, label: string }

Detects the operating system from a user-agent string.

type OS = 'windows' | 'macos' | 'linux' | 'android' | 'ios' | 'unknown'
getOS(navigator.userAgent)
// { type: 'macos', label: 'macOS' }

getOS('Mozilla/5.0 (Windows NT 10.0; Win64; x64)...')
// { type: 'windows', label: 'Windows' }

vCard

VCardGenerator.generate(contact: VCardContact): string

Generates a vCard 4.0 format string.

import { VCardGenerator } from '@gmana/utils'

const vcard = VCardGenerator.generate({
  firstName: 'Sun',
  lastName: 'Sreng',
  email: '[email protected]',
  organization: 'Gmana',
  phone: '+1-555-0100',
})
// 'BEGIN:VCARD\r\nVERSION:4.0\r\n...'

VCardGenerator.createDownloadBlob(contact: VCardContact): Blob

Creates a downloadable .vcf blob for use with a download link.

const blob = VCardGenerator.createDownloadBlob({
  firstName: 'Sun',
  lastName: 'Sreng',
  email: '[email protected]',
})

const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'contact.vcf'
a.click()

License

MIT