{"id":20140053,"url":"https://github.com/jeremy-code/ts-utils","last_synced_at":"2025-09-05T12:39:31.510Z","repository":{"id":226294197,"uuid":"768256560","full_name":"jeremy-code/ts-utils","owner":"jeremy-code","description":"Collection of tested and common utilities for JavaScript/TypeScript","archived":false,"fork":false,"pushed_at":"2024-05-15T21:27:53.000Z","size":587,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-02T09:46:15.667Z","etag":null,"topics":["ts","typescript","typescript-library","util","utilities","utility","utility-function","utility-library","utils","utils-lib","utils-library"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jeremy-code.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-06T18:49:01.000Z","updated_at":"2024-05-15T21:27:56.000Z","dependencies_parsed_at":"2024-03-27T02:24:00.264Z","dependency_job_id":"f6b21be2-918b-4b36-bac7-0f4f73b18a37","html_url":"https://github.com/jeremy-code/ts-utils","commit_stats":null,"previous_names":["jeremy-code/ts-utils"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremy-code%2Fts-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremy-code%2Fts-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremy-code%2Fts-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jeremy-code%2Fts-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jeremy-code","download_url":"https://codeload.github.com/jeremy-code/ts-utils/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241583959,"owners_count":19986074,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ts","typescript","typescript-library","util","utilities","utility","utility-function","utility-library","utils","utils-lib","utils-library"],"created_at":"2024-11-13T21:48:56.298Z","updated_at":"2025-03-02T23:22:08.101Z","avatar_url":"https://github.com/jeremy-code.png","language":"TypeScript","readme":"# ts-utils [![GitHub Actions badge](https://github.com/jeremy-code/ts-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/jeremy-code/ts-utils/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/jeremy-code/ts-utils)](LICENSE)\n\n# Table of Contents\n\n- [string](#string)\n  - [capitalize.ts](#capitalizets)\n  - [randomString.ts](#randomStringts)\n  - [segment.ts](#segmentts)\n  - [truncateMiddle.ts](#truncateMiddlets)\n- [object](#object)\n  - [isEmpty.ts](#isEmptyts)\n  - [isPlainObject.ts](#isPlainObjectts)\n  - [jsonStringifyMap.ts](#jsonStringifyMapts)\n  - [parseFormData.ts](#parseFormDatats)\n  - [parseUrlSearchParams.ts](#parseUrlSearchParamsts)\n  - [pick.ts](#pickts)\n  - [shallowEqual.ts](#shallowEqualts)\n- [number](#number)\n  - [randomNum.ts](#randomNumts)\n  - [relativeError.ts](#relativeErrorts)\n  - [statistics.ts](#statisticsts)\n- [misc](#misc)\n  - [assertIsError.ts](#assertIsErrorts)\n  - [assertNever.ts](#assertNeverts)\n  - [createRangeMapper.ts](#createRangeMapperts)\n  - [store.ts](#storets)\n- [function](#function)\n  - [debounce.ts](#debouncets)\n  - [sleep.ts](#sleepts)\n  - [throttle.ts](#throttlets)\n- [formatting](#formatting)\n  - [formatBytes.ts](#formatBytests)\n  - [formatMs.ts](#formatMsts)\n  - [formatNumber.ts](#formatNumberts)\n  - [formatOrdinal.ts](#formatOrdinalts)\n  - [uri.ts](#urits)\n- [color](#color)\n  - [color.ts](#colorts)\n- [array](#array)\n  - [arrayEqual.ts](#arrayEqualts)\n  - [chunk.ts](#chunkts)\n  - [isIterable.ts](#isIterablets)\n  - [minMax.ts](#minMaxts)\n  - [range.ts](#rangets)\n\n## string\n\n### capitalize.ts\n\n```typescript\n/**\n * @file Utility functions for the intrinsic string manipulation types in\n * TypeScript (e.g. capitalize, uncapitalize, uppercase, lowercase). capitalize\n * and uncapitalize are more useful, but uppercase and lowercase are included\n * for completeness.\n *\n * Functions are locale-insensitive and works best in English, and becomes\n * signficantly more complex if one wants locale sensitivity.\n */\n\n// Preference for .charAt() over array indexing [] in case of empty string \"\"\n\nexport const capitalize = \u003cT extends string\u003e(str: T) =\u003e\n  `${str.charAt(0).toUpperCase()}${str.substring(1)}` as Capitalize\u003cT\u003e;\n\nexport const capitalize1 = ([first, ...rest]: string) =\u003e\n  `${(first ?? \"\").toUpperCase()}${rest.join(\"\")}`;\n\nexport const capitalize2 = (str: string) =\u003e\n  str.charAt(0).toUpperCase() + str.slice(1);\n\nexport const uncapitalize = \u003cT extends string\u003e(str: T) =\u003e\n  `${str.charAt(0).toLowerCase()}${str.substring(1)}` as Uncapitalize\u003cT\u003e;\n\n/**\n * uppercase() and lowercase() functions for completeness. Since they are really\n * simple, simply using .toUpperCase() and .toLowerCase() methods and casting\n * should be preferred instead.\n */\n\nexport const uppercase = \u003cT extends string\u003e(str: T) =\u003e\n  str.toUpperCase() as Uppercase\u003cT\u003e;\n\nexport const lowercase = \u003cT extends string\u003e(str: T) =\u003e\n  str.toLowerCase() as Lowercase\u003cT\u003e;\n```\n\n### randomString.ts\n\n````typescript\nconst CHARACTERS =\n  \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n\n/**\n * For flexibility and/or to flex, CHARACTERS can be generated programmatically\n *\n * @example\n * ```ts\n * const CHARACTERS = Array.from(\n *   { length: 62 }, // 26 + 26 + 10\n *   (_, i) =\u003e\n *     i \u003c 26\n *       ? String.fromCharCode(i + 65) // uppercase letters ('A' = 65)\n *       : i \u003c 52 // 26 + 26\n *         ? String.fromCharCode(i + 71) // lowercase letters ('a' = 97)\n *         : String.fromCharCode(i - 4), // numbers ('0' = 48)\n * ).join(\"\");\n * ```\n */\n\nexport const randomString = (length: number, characters = CHARACTERS) =\u003e\n  Array.from(\n    // Using crypto.getRandomValues() for better randomness/security\n    crypto.getRandomValues(new Uint8Array(length)),\n    (byte) =\u003e characters[byte % characters.length],\n  ).join(\"\");\n````\n\n### segment.ts\n\n```typescript\n/**\n * Using Intl.Segmenter to segment a string into an array of substrings, either\n * by grapheme, word, or sentence. Effectively, a way more reliable way of doing\n * input.split(\"\"), input.split(\" \"), or input.split(\".\") respectively. (since\n * those methods are not reliable for all languages).\n *\n * @remark Default option is to segment by grapheme (letter for alphabetical scripts)\n */\nexport const segment = (\n  input: string,\n  ...[locales, options]: ConstructorParameters\u003ctypeof Intl.Segmenter\u003e\n) =\u003e\n  Array.from(new Intl.Segmenter(locales, options).segment(input)).map(\n    (s) =\u003e s.segment,\n  );\n\n/**\n * Segment a string into an array of words. Filters out non-word-like segments\n * (e.g. punctuation marks). More reliable than input.split(\" \").\n */\nexport const segmentByWord = (\n  input: string,\n  // if not interested in locale-specific word segmentation, may be ommitted and\n  // just set locale to undefined and do not provide options\n  locales?: Intl.LocalesArgument,\n  options?: Omit\u003cIntl.SegmenterOptions, \"granularity\"\u003e, // exclude granularity, always \"word\"\n) =\u003e\n  Array.from(\n    new Intl.Segmenter(locales, {\n      granularity: \"word\",\n      ...options,\n    }).segment(input),\n  )\n    // Alternatively can use .reduce() to do so in single iteration\n    // e.g. arr.reduce((acc, s) =\u003e (s.isWordLike ? [...acc, s.segment] : acc), [])\n    .filter((s) =\u003e s.isWordLike)\n    .map((s) =\u003e s.segment);\n```\n\n### truncateMiddle.ts\n\n```typescript\n// Use text-overflow: ellipsis in CSS to truncate text at the end of a string.\n\n/**\n * Truncate a string to at most `targetLength` in the middle, keeping the first and\n * last characters while inserting a placeholder in the middle.\n *\n * Useful for hrefs, filenames, etc. where the start and end of the string are\n * important.\n *\n * In the future, this may not be necessary depending on CSS support, see:\n * {@link https://github.com/w3c/csswg-drafts/issues/3937}\n *\n * @example truncateMiddle(\"1234567890\", 5) // \"1...0\"\n */\nexport const truncateMiddle = (\n  str: string,\n  targetLength: number,\n  // May want to set default placeholder to \"…\" (unicode ellipsis character)\n  // instead of \"...\" (three dots)\n  placeholder = \"...\",\n) =\u003e\n  str.length \u003e targetLength ?\n    str.slice(0, Math.ceil((targetLength - placeholder.length) / 2)) +\n    placeholder +\n    str.slice(-Math.floor((targetLength - placeholder.length) / 2))\n  : str; // if targetLength is less than or equal to str.length, string will be returned as-is\n```\n\n## object\n\n### isEmpty.ts\n\n```typescript\n/**\n * Roughly equivalent to `lodash.isempty`, which is the 2489th most popular\n * package with 2M weekly downloads\n */\n\nexport const isEmpty = (value: unknown) =\u003e {\n  if (value === null || value === undefined) return true;\n  if (Array.isArray(value) || typeof value === \"string\")\n    return value.length === 0; // [] or \"\"\n  if (value instanceof Map || value instanceof Set) return value.size === 0;\n  // Must be last in case of Array, Map, Set\n  if (typeof value === \"object\") return Object.entries(value).length === 0; // {}\n\n  return false;\n};\n\n// Using many nested ternaries rather than early return. Arguably less readable.\nexport const isEmpty1 = (value: unknown) =\u003e\n  value === null ||\n  value === undefined ||\n  (Array.isArray(value) || typeof value === \"string\" ? value.length === 0\n  : value instanceof Map || value instanceof Set ? value.size === 0\n  : typeof value === \"object\" ? Object.entries(value).length === 0\n  : false);\n```\n\n### isPlainObject.ts\n\n````typescript\n/**\n * Walks up the prototype chain to find the base prototype, then compares it to\n * the original prototype. If they are the same, it is a plain object.\n *\n * Equivalent to `lodash.isplainobject`, which is the 382th most popular package with\n * 15M weekly downloads\n *\n * The signficantly easier function would be the following, but for cross-realm\n * objects, where the identity of the Object prototype is different, it would\n * fail:\n *\n * @example\n * ```ts\n * const isPlainObj = (value: unknown) =\u003e\n *   !!value \u0026\u0026 Object.getPrototypeOf(value) === Object.prototype;\n * ```\n */\nexport function isPlainObject(obj: unknown) {\n  if (typeof obj !== \"object\" || obj === null) {\n    return false;\n  }\n\n  const proto = Object.getPrototypeOf(obj) as object | null;\n\n  // Null prototype (e.g. `Object.create(null)`)\n  if (proto === null) {\n    return true;\n  }\n\n  // Walks up the prototype chain to find the base prototype\n  let baseProto = proto;\n  while (Object.getPrototypeOf(baseProto) !== null) {\n    baseProto = Object.getPrototypeOf(baseProto) as object;\n  }\n\n  return proto === baseProto;\n}\n````\n\n### jsonStringifyMap.ts\n\n```typescript\n/**\n * Unfortunately, JSON.stringify does not support Native ES6 Map. A proposal was\n * made {@link https://github.com/DavidBruant/Map-Set.prototype.toJSON} but was\n * rejected in favor of a custom replacer function.\n *\n * An alternative is to extend the Map object with a custom toJSON() method, but\n * extending native JS objects is not recommended.\n *\n * Since this can be implemented in many different ways, here is an opinionated\n * implementation with a replacer and a reviver function.\n *\n * Note that while this is also true for Set, converting between Set and Array\n * is trivial (Array.from(set) and new Set(arr)), which is probably more\n * efficient than a replacer/reviver.\n */\n\ntype ObjectifiedMap = {\n  type: \"Map\";\n  value: [unknown, unknown][];\n};\n\n// Type predicate to check if an object is an ObjectifiedMap\n// If only parsing internal data, a cast may be more appropriate\nconst isObjectifiedMap = (value: unknown): value is ObjectifiedMap =\u003e\n  typeof value === \"object\" \u0026\u0026\n  value !== null \u0026\u0026\n  \"type\" in value \u0026\u0026\n  value.type === \"Map\" \u0026\u0026\n  \"value\" in value \u0026\u0026\n  Array.isArray(value.value);\n\n// Using unknown rather than the default any for value. Doesn't make a\n// difference besides avoiding implicit any\nexport const mapReplacer = (_key: string, value: unknown) =\u003e\n  value instanceof Map ?\n    {\n      type: \"Map\",\n      value: Array.from(value),\n    }\n  : value;\n\nexport const mapReviver = (_key: string, value: unknown) =\u003e\n  isObjectifiedMap(value) ? new Map(value.value) : value;\n```\n\n### parseFormData.ts\n\n```typescript\n/**\n * Parses FormData object into a plain object.\n */\nexport const parseFormData = (formData: FormData) =\u003e\n  Array.from(formData).reduce\u003c\n    Record\u003cstring, FormDataEntryValue | FormDataEntryValue[]\u003e\n  \u003e((acc, [k, v]) =\u003e {\n    if (!acc[k]) {\n      // Returns array only if there are multiple values\n      const values = formData.getAll(k);\n      acc[k] = values.length \u003e 1 ? values : v;\n    }\n    return acc;\n  }, {});\n```\n\n### parseUrlSearchParams.ts\n\n```typescript\n/**\n * Parse URLSearchParams to plain object.\n */\nexport const parseUrlSearchParams = (urlSearchParams: URLSearchParams) =\u003e\n  Array.from(urlSearchParams).reduce\u003cRecord\u003cstring, string | string[]\u003e\u003e(\n    (acc, [k, v]) =\u003e {\n      if (!acc[k]) {\n        // Returns array only if there are multiple values\n        const values = urlSearchParams.getAll(k);\n        acc[k] = values.length \u003e 1 ? values : v;\n      }\n      return acc;\n    },\n    {},\n  );\n```\n\n### pick.ts\n\n```typescript\n/**\n * @file pick() and omit() utility functions for objects, equivalent to the\n * TypeScript utility types `Pick` and `Omit`.\n */\n\n// Equivalent to pick utility type in TypeScript and lodash.pick\nexport const pick = \u003cT extends object, K extends keyof T\u003e(\n  obj: T,\n  keys: readonly K[],\n): Pick\u003cT, K\u003e =\u003e\n  // Casting to Pick\u003cT, K\u003e is necessary because Object.fromEntries returns { [k:\n  // string]: T; }, so as long as keys are only strings, cast should be safe\n  Object.fromEntries(keys.map((key) =\u003e [key, obj[key]])) as Pick\u003cT, K\u003e;\n\n/**\n * Omit is not as common as pick, but here for completeness.\n *\n * Mutable approach, which imo is more readable. May want to change type from {\n * [key: string]: unknown } to something else (such as key: PropertyKey)\n * depending on use case.\n */\n\nexport function omit\u003cT extends { [key: string]: unknown }, K extends keyof T\u003e(\n  object: T,\n  keys: K[],\n): Omit\u003cT, K\u003e {\n  const omitted: { [key: string]: unknown } = {};\n  Object.keys(object).forEach((key) =\u003e {\n    if (!keys.includes(key as K)) {\n      omitted[key] = object[key];\n    }\n  });\n  return omitted as Omit\u003cT, K\u003e;\n}\n\n// Immutable approach by chaining Object.entries and Object.fromEntries\nexport const omit1 = \u003cT extends { [key: string]: unknown }, K extends keyof T\u003e(\n  object: T,\n  keys: K[],\n): Omit\u003cT, K\u003e =\u003e\n  Object.fromEntries(\n    Object.entries(object).filter(([key]) =\u003e !keys.includes(key as K)),\n  ) as Omit\u003cT, K\u003e;\n```\n\n### shallowEqual.ts\n\n```typescript\n/**\n * Compare two objects one level deep (shallow comparison / Object.is)\n *\n * Roughly equivalent to npm package `shallowequal` which is the 1849th most\n * popular package with 5M weekly downloads\n *\n * Based on React's default implementation of shallowEqual\n * {@see https://github.com/facebook/react/blob/main/packages/shared/shallowEqual.js }\n */\n\nexport const shallowEqual = \u003cT extends Record\u003cstring, unknown\u003e\u003e(\n  obj1: T,\n  obj2: T,\n) =\u003e {\n  if (Object.is(obj1, obj2)) return true;\n  if (\n    typeof obj1 !== \"object\" ||\n    obj1 === null ||\n    typeof obj2 !== \"object\" ||\n    obj2 === null\n  ) {\n    return false;\n  }\n\n  const keys1 = Object.keys(obj1);\n  const keys2 = Object.keys(obj2);\n\n  return (\n    keys1.length === keys2.length \u0026\u0026\n    keys1.every((key) =\u003e Object.is(obj1[key], obj2[key]))\n  );\n};\n```\n\n## number\n\n### randomNum.ts\n\n```typescript\n/**\n * Generates a random integer between min and max (inclusive) using Math.random.\n *\n * Some may use Math.round (e.g. `Math.round(Math.random() * (max - min) +\n * min)`) but this skews the distribution.\n *\n * @example randomNum(1,6) // 4\n */\nexport const randomNum = (min: number, max: number) =\u003e\n  Math.floor(\n    Math.random() *\n      // Remove `+ 1` to make the max exclusive\n      (Math.floor(max) - Math.ceil(min) + 1) +\n      Math.ceil(min),\n  );\n```\n\n### relativeError.ts\n\n```typescript\nexport const relativeError = (actual: number, expected: number) =\u003e\n  // if expected is 0, returns NaN\n  Math.abs((actual - expected) / expected);\n\n// For completeness. Inlining the function `Math.abs(actual - expected)` is\n// probably clearer\nexport const absoluteError = (actual: number, expected: number) =\u003e\n  Math.abs(actual - expected);\n```\n\n### statistics.ts\n\n```typescript\n/**\n * @file The basic statistics functions for numbers, expanding on what the Math\n * object provides. Realistically, you shouldn't need ALL of these functions,\n * but they are included for completeness. You likely want to use d3-array or\n * similar library for more complicated statistics.\n */\n\n// min = Math.min\n// max = Math.max\n// range = Math.max - Math.min\n\n/**\n * For now, sum is inlined when needed (e.g. .reduce((a, b) =\u003e a + b, 0)). This\n * is somewhat imprecise for groups of floating point numbers, but generally\n * fine for most use cases.\n *\n * In the future, Math.sumPrecise(), which is in Stage 2.7, may be avaliable and\n * may be used with some performance tradeoffs.\n *\n * {@link https://github.com/tc39/proposal-math-sum}\n */\n\nexport const mean = (...values: number[]) =\u003e\n  values.length === 0 ?\n    undefined // Matches standard for array functions\n  : values.reduce((acc, value) =\u003e acc + value, 0) / values.length;\n\nexport const median = (...values: number[]) =\u003e {\n  if (values.length === 0) return undefined;\n\n  // .sorted is standard ES2023, using .sort() which mutates array is also fine\n  const sorted = values.toSorted((a, b) =\u003e a - b);\n  const mid = Math.floor(sorted.length / 2);\n\n  return sorted.length % 2 !== 0 ?\n      sorted[mid]\n      // Should never be undefined, `?? 0` for noUncheckedIndexedAccess\n    : ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2;\n};\n\nexport const mode = (...values: number[]) =\u003e {\n  if (values.length === 0) return undefined;\n\n  const counts = values.reduce(\n    (acc, v) =\u003e acc.set(v, (acc.get(v) || 0) + 1),\n    new Map\u003cnumber, number\u003e(),\n  );\n  const maxCount = Math.max(...counts.values());\n\n  return Array.from(counts).reduce\u003cnumber[]\u003e(\n    (acc, [value, count]) =\u003e (count === maxCount ? [...acc, value] : acc),\n    [],\n  );\n  // Alternatively, chain .filter().map() instead of reduce() for readability\n  // .filter(([, count]) =\u003e count === maxCount).map(([value]) =\u003e value);\n\n  // May use .find() if only need first element with the highest count\n};\n\nexport const variance = (...values: number[]) =\u003e {\n  if (values.length === 0) return undefined;\n\n  const mean = values.reduce((acc, value) =\u003e acc + value, 0) / values.length;\n  return (\n    values.reduce((acc, value) =\u003e acc + (value - mean) ** 2, 0) / values.length\n  );\n};\n\n// Could use `variance()` for this calculation to simplify code\nexport const standardDeviation = (...values: number[]) =\u003e {\n  if (values.length === 0) return undefined;\n\n  const mean = values.reduce((acc, value) =\u003e acc + value, 0) / values.length;\n  const variance =\n    values.reduce((acc, value) =\u003e acc + (value - mean) ** 2, 0) / values.length;\n\n  return Math.sqrt(variance);\n};\n```\n\n## misc\n\n### assertIsError.ts\n\n```typescript\n/**\n * Useful in try/catch blocks to ensure `error: unknown` is an Error without\n * having to wrap error handling in `if (error instanceof Error)`\n */\nexport function assertIsError(error: unknown): asserts error is Error {\n  if (!(error instanceof Error)) {\n    throw new Error(`Expected an Error but got ${typeof error}`, {\n      cause: error,\n    });\n  }\n}\n```\n\n### assertNever.ts\n\n```typescript\n/**\n * Enforces that a value is never reached (e.g. in a switch statement). Useful\n * for exhaustiveness.\n */\nexport function assertNever(value: never, message?: string): never {\n  // JSON.stringify is not perfect, such as for Map, Set, but works for most\n  // cases, and avoids issues working with `never`\n  throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`, {\n    cause: value,\n  });\n}\n```\n\n### createRangeMapper.ts\n\n```typescript\ntype Range = [start: number, end: number];\n\nexport const createRangeMapper = \u003cT extends PropertyKey\u003e(\n  map: Record\u003cT, Range\u003e,\n) =\u003e {\n  return (value: number) =\u003e {\n    const entry = Object.entries\u003cRange\u003e(map).find(\n      ([, [start, end]], i) =\u003e\n        i === 0 ?\n          // Inclusive of start and end on first entry (e.g. on A: [90, 100], 100 would be an A)\n          value \u003e= start \u0026\u0026 value \u003c= end\n        : start \u003c= value \u0026\u0026 value \u003c end, // exclusive of end on subsequent entries\n    );\n\n    if (!entry) {\n      throw new RangeError(\n        `Invalid value with no corresponding range: ${value}`,\n        {\n          cause: { map, value },\n        },\n      );\n    }\n\n    return entry[0] as T;\n  };\n};\n```\n\n### store.ts\n\n```typescript\n/**\n * Intended for use in React with `useSyncExternalStore()`. For more complex use\n * cases, see a library like Zustand.\n *\n * A start for a simple store implementation. Notably, it wouldn't actually work\n * by itself. Since the state is mutated instead of replaced, React would never\n * re-render, since it passes Object.is equality due to having same reference.\n */\nexport function createStore\u003cS\u003e(initialState: S | (() =\u003e S)) {\n  // Matches useState's behavior\n  let state =\n    typeof initialState === \"function\" ?\n      (initialState as () =\u003e S)()\n    : initialState;\n  const listeners = new Set\u003c() =\u003e void\u003e();\n\n  // Using an object with methods. Class may be more appropriate.\n  return {\n    getSnapshot: () =\u003e state,\n    subscribe: (onStoreChange: () =\u003e void) =\u003e {\n      listeners.add(onStoreChange);\n\n      return () =\u003e listeners.delete(onStoreChange);\n    },\n    update: (newState: Partial\u003cS\u003e) =\u003e {\n      state = { ...state, ...newState };\n      listeners.forEach((listener) =\u003e listener());\n    },\n  };\n}\n\n// Would have to be initialized somewhere to be used:\nexport const store = createStore({ count: 0 });\n```\n\n## function\n\n### debounce.ts\n\n```typescript\nexport function debounce\u003cT extends (...args: Parameters\u003cT\u003e) =\u003e ReturnType\u003cT\u003e\u003e(\n  callback: T,\n  ms?: number,\n  immediate?: boolean,\n) {\n  let timeoutId: NodeJS.Timeout | undefined;\n\n  return function (this: ThisParameterType\u003cT\u003e, ...args: Parameters\u003cT\u003e): void {\n    const later = () =\u003e {\n      timeoutId = undefined;\n      if (!immediate) callback.apply(this, args);\n    };\n\n    const callNow = immediate \u0026\u0026 timeoutId === undefined;\n    clearTimeout(timeoutId);\n    timeoutId = setTimeout(later, ms);\n\n    if (callNow) callback.apply(this, args);\n  };\n}\n```\n\n### sleep.ts\n\n```typescript\nexport const sleep = (ms?: number) =\u003e\n  new Promise((resolve) =\u003e setTimeout(resolve, ms));\n```\n\n### throttle.ts\n\n```typescript\n/**\n * Throttle a function to be called at most once every `ms` milliseconds.\n */\nexport function throttle\u003cT extends (...args: Parameters\u003cT\u003e) =\u003e ReturnType\u003cT\u003e\u003e(\n  callback: T,\n  ms: number,\n  immediate?: boolean,\n) {\n  let lastTime = immediate ? -ms : 0;\n\n  return function (...args: Parameters\u003cT\u003e) {\n    const now = Date.now();\n    if (now - lastTime \u003e= ms) {\n      callback(...args);\n      lastTime = now;\n    }\n  };\n}\n```\n\n## formatting\n\n### formatBytes.ts\n\n```typescript\n/**\n * @file Functions to format bytes into human-readable strings using\n * Intl.NumberFormat. A lot of implementations use some kind of number division\n * and modulo to determine the appropriate unit, but this is more flexible\n * depending on locale.\n *\n * Since each unit multiple is considered a separate unit, we have to manually\n * determine the appropriate unit and corresponding value, otherwise we get\n * formatting such as \"1 BB\" instead of \"1 GB\".\n *\n * Roughly equivalent to `pretty-bytes` package (ranked 916 in npm popularity,\n * 11M weekly downloads)\n */\n\n/**\n * Per {@link https://tc39.es/ecma402/#table-sanctioned-single-unit-identifiers},\n * these are the valid byte units supported by the `Intl.NumberFormat` API.\n */\nconst UNITS = [\n  \"byte\",\n  \"kilobyte\",\n  \"megabyte\",\n  \"gigabyte\",\n  \"terabyte\",\n  \"petabyte\",\n];\n\n// Alternatively, though less succinct/descriptive\n// const UNITS = [\"\", \"kilo\", \"mega\", \"giga\", \"tera\", \"peta\"].map(\n//   (prefix) =\u003e `${prefix}byte`,\n// );\n\n// SI units, where 1 gigabyte = 1000 megabytes\nexport const formatBytes = (\n  bytes: number,\n  ...[locales, options]: ConstructorParameters\u003cIntl.NumberFormatConstructor\u003e\n): string =\u003e {\n  // Negative bytes doesn't really make sense. Not technically correct for\n  // Intl.NumberFormat since doesn't obey signDisplay option, but as previously\n  // stated, negative bytes doesn't make sense.\n  if (Math.sign(bytes) === -1) return `-${formatBytes(Math.abs(bytes))}`;\n\n  const exponent =\n    // 0 becomes -Infinity, nonfinite numbers cannot index UNITS\n    bytes !== 0 \u0026\u0026 Number.isFinite(bytes) ?\n      Math.min(\n        Math.floor(Math.log10(bytes) / 3),\n        UNITS.length - 1, // set to max unit if exponent exceeds largest unit (i.e. petabyte)\n      )\n    : 0; // defaults to unit \"byte\"\n\n  const value = bytes / 1000 ** exponent;\n\n  // Initializes new NumberFormat instance every time, may want to use one\n  // instance if using frequently for caching\n  return new Intl.NumberFormat(locales, {\n    style: \"unit\",\n    unit: UNITS[exponent],\n    ...options,\n  }).format(value);\n};\n\n/**\n * Alternatively, using Binary rather than SI units, where 1 gigabyte = 1024\n * megabytes. Technically, should be using kibibytes, mebibytes, etc., but these\n * are not supported units in ECMA-402.\n */\nexport const formatBytesBinary = (\n  bytes: number,\n  ...[locales, options]: ConstructorParameters\u003cIntl.NumberFormatConstructor\u003e\n): string =\u003e {\n  if (Math.sign(bytes) === -1) return `-${formatBytesBinary(Math.abs(bytes))}`;\n\n  const exponent =\n    Number.isFinite(bytes) \u0026\u0026 bytes !== 0 ?\n      Math.min(Math.floor(Math.log2(bytes) / 10), UNITS.length - 1)\n    : 0;\n\n  const value = bytes / 1024 ** exponent;\n\n  return new Intl.NumberFormat(locales, {\n    style: \"unit\",\n    unit: UNITS[exponent],\n    ...options,\n  }).format(value);\n};\n```\n\n### formatMs.ts\n\n```typescript\n/**\n * @file Format milliseconds into human-readable strings. Similar to packages\n * `ms`, `pretty-ms`, `@lukeed/ms`, etc, albeit without parsing capabilities\n * (such would make the locale handling more complex and also likely involve\n * complex regex).\n *\n * Using `Intl.NumberFormat`, though `Intl.RelativeTimeFormat` may be more\n * appropriate depending on use case.\n */\n\n// Constants for time conversion. Separated into individual constants, so they\n// can be used with each other (rather than having to do 1000 * 60 * 60 * 24)\n// and to do conversions in other files (e.g. const days = ms / MS_PER_DAY)\n\nexport const MS_PER_SECOND = 1000; // 1000ms = 1s\nexport const MS_PER_MINUTE = 60 * MS_PER_SECOND; // 60s = 1min\nexport const MS_PER_HOUR = 60 * MS_PER_MINUTE; // 60min = 1hr\nexport const MS_PER_DAY = 24 * MS_PER_HOUR; // 24hr = 1 day\n\n/**\n * Per {@link https://tc39.es/ecma402/#table-sanctioned-single-unit-identifiers},\n * these are the valid byte units supported by the `Intl.NumberFormat` API.\n */\nconst UNITS: Record\u003cstring, number\u003e = {\n  // Units smaller than ms (e.g. microsecond, nanosecond) may be added here, but\n  // since the standard libraries don't tend to support them, they are omitted\n  // for consistency.\n  second: MS_PER_SECOND,\n  minute: MS_PER_MINUTE,\n  hour: MS_PER_HOUR,\n  day: MS_PER_DAY,\n  // Likewise, week, month, etc. could also be added here. Note that due to\n  // calendars, months/years are not fixed units.\n};\n\nexport const formatMs = (\n  ms: number,\n  ...[locales, options]: ConstructorParameters\u003cIntl.NumberFormatConstructor\u003e\n): string =\u003e {\n  const unit = Object.entries(UNITS).reduce(\n    // Find smallest unit that fits given ms\n    (acc, [unit, msPerUnit]) =\u003e (ms / msPerUnit \u003e= 1 ? unit : acc),\n    \"millisecond\", // Defaults to ms if unit not found\n  );\n\n  return Intl.NumberFormat(locales, {\n    style: \"unit\",\n    unit,\n    // For similar output to `ms` package, use options:\n    // notation: \"compact\",\n    // unitDisplay: \"narrow\",\n    ...options,\n  }).format(ms / (UNITS[unit] ?? 1));\n};\n\n/**\n * Exact time formatting where ms are rounded to the nearest unit\n *\n * Similar to `pretty-ms` package\n *\n * In the future, using `Intl.DurationFormat` (which is in Stage 3) may be more\n * appropriate, but it is currently only supported in Safari.\n */\n\nexport const formatMsExact = (\n  ms: number,\n  ...[locales, options]: Parameters\u003cnumber[\"toLocaleString\"]\u003e\n): string =\u003e\n  // Effectively, in English, join formatted units with space. Since there are\n  // only 3 options for Intl.ListFormat, it seems unnecessary to have its own\n  // option parameter\n  new Intl.ListFormat(locales, {\n    type: \"unit\",\n    style: \"narrow\", // Update to \"short\" to add list separators (commas in English)\n  }).format(\n    // Did not include UNITS[\"millisecond\"] since it seems redundant, but can be\n    // added to constant UNITS if needed\n    [[\"millisecond\", 1] as const, ...Object.entries(UNITS)].reduceRight\u003c\n      string[]\n    \u003e((acc, [unit, msPerUnit]) =\u003e {\n      if (ms \u003e= msPerUnit) {\n        const unitsPerMs = Math.floor(ms / msPerUnit);\n        ms %= msPerUnit; // remainder, update ms\n        // Using concat to avoid mutating array, but push is also valid\n        acc = acc.concat([\n          unitsPerMs.toLocaleString(locales, {\n            style: \"unit\",\n            unit,\n            // For similar output to `pretty-ms` package, use these options:\n            // notation: \"compact\", unitDisplay: \"narrow\",\n            ...options,\n          }),\n        ]);\n      }\n      return acc;\n    }, []),\n  );\n```\n\n### formatNumber.ts\n\n```typescript\n/**\n * Coerces a value to a number and formats it using the provided options.\n */\nexport const formatNumber = (\n  value: unknown,\n  ...params: ConstructorParameters\u003cIntl.NumberFormatConstructor\u003e\n) =\u003e new Intl.NumberFormat(...params).format(Number(value));\n```\n\n### formatOrdinal.ts\n\n```typescript\n// Only true for English, becomes significantly more complex for other languages\nconst SUFFIXES = {\n  zero: \"th\",\n  one: \"st\",\n  two: \"nd\",\n  few: \"rd\",\n  other: \"th\",\n  many: \"th\", // Should never occur in English, included for TypeScript\n} satisfies Record\u003cIntl.LDMLPluralRule, string\u003e;\n\n/**\n * Formats a number as an ordinal (e.g. 1st, 2nd, 3rd, 4th).\n *\n * @example formatOrdinal(1) // \"1st\"\n */\nexport const formatOrdinal = (\n  num: number,\n  ...[locales, options]: ConstructorParameters\u003cIntl.PluralRulesConstructor\u003e\n) =\u003e {\n  const suffix =\n    SUFFIXES[\n      new Intl.PluralRules(locales, {\n        type: \"ordinal\",\n        ...options,\n      }).select(num)\n    ];\n\n  // Options are not passed to `toLocaleString` and using the shared parameters\n  // of Intl.PluralRules. Feel free to add as an parameter if needed.\n  return `${num.toLocaleString(locales, options)}${suffix}`;\n};\n```\n\n### uri.ts\n\n```typescript\n/**\n * A tagged template literal for encoding URI components.\n *\n * In the future, this may be simplified with the `String.cooked` proposal\n * {@link https://github.com/tc39/proposal-string-cooked}\n *\n * @example uri`https://example.com/${name}` // https://example.com/John%20Doe\n */\nexport const uri = (\n  template: TemplateStringsArray,\n  // valid values for encodeURIComponent\n  ...values: (string | number | boolean)[]\n) =\u003e String.raw({ raw: template }, ...values.map((v) =\u003e encodeURIComponent(v)));\n```\n\n## color\n\n### color.ts\n\n```typescript\n/**\n * Some color utilities. Unfortunately, color manipulation in general is really\n * complicated, and a task more befitting of a library than a handful of utility\n * functions.\n *\n * If you need to do anything more complicated than what's here, see the\n * libraries: color, color-string, d3-color, colord, tinycolor2, chroma-js, etc.\n * Or, if you're using a CSS-in-JS library, it might have color utilities built\n * in.\n *\n * If you want to see a more complex set of color manipulation utils, see\n * {@link https://github.com/microsoft/vscode/blob/main/src/vs/base/common/color.ts}\n */\n\n// No support for alpha channel/transparency\ntype RGB = {\n  r: number;\n  g: number;\n  b: number;\n};\n\nexport const hexToRgb = (hex: string): RGB =\u003e {\n  const hexValue = hex.startsWith(\"#\") ? hex.slice(1) : hex;\n  // Expand shorthand hex values (e.g. #fff -\u003e #ffffff)\n  const fullHex =\n    hexValue.length === 3 || hexValue.length === 4 ?\n      [...hexValue].map((char) =\u003e char.repeat(2)).join(\"\")\n    : hexValue;\n\n  const bigint = parseInt(fullHex, 16);\n\n  return {\n    r: (bigint \u003e\u003e 16) \u0026 255,\n    g: (bigint \u003e\u003e 8) \u0026 255,\n    b: bigint \u0026 255,\n  };\n};\n\nexport const rgbToHex = ({ r, g, b }: RGB) =\u003e\n  `#${[r, g, b].map((x) =\u003e x.toString(16).padStart(2, \"0\")).join(\"\")}`;\n```\n\n## array\n\n### arrayEqual.ts\n\n```typescript\nexport const arrayEqual = (value1: unknown[], value2: unknown[]) =\u003e\n  value1.length === value2.length \u0026\u0026\n  value1.every((val, index) =\u003e Object.is(val, value2[index]));\n\nexport const nestedArrayEqual = \u003cT\u003e(value1: T[], value2: T[]): boolean =\u003e {\n  if (value1.length !== value2.length) {\n    return false;\n  }\n\n  return value1.every((val, index) =\u003e {\n    // for TypeScript inference\n    const value2Entry = value2[index];\n\n    return Array.isArray(val) \u0026\u0026 Array.isArray(value2Entry) ?\n        nestedArrayEqual(val, value2Entry)\n      : Object.is(val, value2Entry);\n  });\n};\n```\n\n### chunk.ts\n\n```typescript\n// Immutable approach\nexport const chunk = \u003cT\u003e(items: T[], size: number) =\u003e\n  items.reduce\u003cT[][]\u003e((arr, item, i) =\u003e {\n    return i % size === 0 ?\n        [...arr, [item]]\n      : [...arr.slice(0, -1), [...(arr.slice(-1)[0] || []), item]];\n  }, []);\n\n// Mutable approach, slightly faster and arguably cleaner code\nexport const chunk1 = \u003cT\u003e(items: T[], size: number) =\u003e {\n  const result: T[][] = [];\n  for (let i = 0; i \u003c items.length; i += size) {\n    result.push(items.slice(i, i + size));\n  }\n  return result;\n};\n```\n\n### isIterable.ts\n\n```typescript\n/**\n * Check if a value is an iterable object (implements the iterable protocol).\n *\n * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol}\n *\n * @example isIterable(new Set()) // true\n */\nexport const isIterable = (value: unknown): value is Iterable\u003cunknown\u003e =\u003e\n  typeof value === \"object\" \u0026\u0026\n  value !== null \u0026\u0026 // typeof null === 'object'\n  Symbol.iterator in value \u0026\u0026\n  typeof value[Symbol.iterator] === \"function\";\n\n/**\n * Check if a value is an array-like object (is an object and has a length property).\n *\n * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#working_with_array-like_objects}\n *\n * @example isArrayLike({ length: 0 }) // true\n */\nexport const isArrayLike = (value: unknown): value is ArrayLike\u003cunknown\u003e =\u003e\n  typeof value === \"object\" \u0026\u0026\n  value !== null \u0026\u0026 // typeof null === 'object'\n  \"length\" in value \u0026\u0026\n  typeof value.length === \"number\";\n```\n\n### minMax.ts\n\n```typescript\n// Explicit type to name tuple elements\ntype Range = [min: number, max: number];\n\n/**\n * Find the minimum and maximum values in an array of numbers.\n *\n * Unlike [Math.min(), Math.max()], this function only iterates through the\n * array once, more suitable for large arrays (and prevents stack overflow\n * errors, since Math.min/max is variadic).\n *\n * @example minMax([1, 2, 3, 4, 5]) // [1, 5]\n */\nexport const minMax = (arr: number[]) =\u003e\n  arr.reduce\u003cRange\u003e(\n    (acc, item) =\u003e {\n      acc[0] = Math.min(acc[0], item);\n      acc[1] = Math.max(acc[1], item);\n      return acc;\n    },\n    // By default, [min, max] = [Infinity, -Infinity] to handle empty arrays\n    [Infinity, -Infinity],\n  );\n```\n\n### range.ts\n\n```typescript\n/**\n * Generate an array of numbers from start to end (inclusive) with optional step\n *\n * Roughly equivalent to `Iterable.range()` proposal in Stage 2, see\n * {@link https://github.com/tc39/proposal-iterator.range}\n *\n * @example range(1, 5) // [1, 2, 3, 4, 5]\n */\nexport const range = (start: number, end: number, step = 1) =\u003e {\n  const length =\n    Math.sign(step) === 1 ?\n      Math.max(Math.ceil((end - start + 1) / step), 0)\n      // If step is negative, go backwards.\n      // Alternatively, may be removed and use .toReversed() when needed\n    : Math.max(Math.ceil((start - end + 1) / Math.abs(step)), 0);\n\n  /**\n   * Performance-wise, new Array().map() is significantly faster than\n   * Array.from(), but Array constructor is often discouraged due to weird\n   * behavior\n   *\n   * @see https://google.github.io/styleguide/tsguide.html#array-constructor\n   * @see https://jsbench.me/lxlv8rn8kd\n   */\n  return Array.from({ length }, (_, index) =\u003e start + index * step);\n};\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremy-code%2Fts-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeremy-code%2Fts-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeremy-code%2Fts-utils/lists"}