/** * Helper functions that have to do with objects. * * @module */ import type { ReadonlyRecord } from "../types/ReadonlyRecord.js"; import type { WidenLiteral } from "../types/WidenLiteral.js"; /** * Helper function to get the keys and values of an object with their literal key types preserved. * * This is a typed wrapper around `Object.entries`. The type assertion is necessary because * TypeScript defines the keys from `Object.entries` as being `string[]`. */ export function getObjectEntries( object: T, ): ReadonlyArray<{ [K in keyof T]: [K, T[K]] }[keyof T]> { return Object.entries(object) as Array< { [K in keyof T]: [K, T[K]]; }[keyof T] >; } /** * Helper function to get keys of an object with their literal key types preserved. * * This is a typed wrapper around `Object.keys`. The type assertion is necessary because TypeScript * defines `Object.keys` as returning `string[]`. */ export function getObjectKeys( object: T, ): ReadonlyArray { return Object.keys(object) as Array; } /** * Safely gets a value from a widened object. * * This is useful when normal indexing produces a type error from an object that uses an `as const` * assertion. */ export function getWidenedObjectValue>( object: T, key: WidenLiteral, ): T[keyof T] | undefined { const widenedObject = object as Record; return widenedObject[key]; } /** * Helper function to get the values in an object that match an arbitrary condition. Similar to the * `Array.filter` method, but works for objects. * * This is efficient such that it avoids converting the object values into an array. */ export function objectFilter( object: ReadonlyRecord, predicate: (value: V) => boolean, ): readonly V[] { const array: V[] = []; // eslint-disable-next-line complete/no-for-in for (const key in object) { const value = object[key]; // eslint-disable-line unicorn/no-unsafe-property-key const match = predicate(value); if (match) { array.push(value); } } return array; } /** * Helper function to map the values in an object to another object with new values. Similar to the * `Array.map` method, but works for objects. */ export function objectMap( object: Record, callback: (key: K, value: V) => U, ): ReadonlyRecord { const entries = Object.entries(object); const mappedEntries = entries.map(([key, value]) => [ key, callback(key as K, value as V), ]); return Object.fromEntries(mappedEntries) as Record; } /** * Helper function to convert an object to a `Map`. * * This is useful when you need to construct a type safe object with the `satisfies` operator, but * then later on you need to query it in a way where you expect the return value to be T or * undefined. In this situation, by converting the object to a map, you can avoid unsafe type * assertions. * * Note that the converted map will only have string keys, due to the nature of JavaScript objects * only having string keys under the hood. */ export function objectToMap( object: Record, ): ReadonlyMap { const map = new Map(); for (const [key, value] of Object.entries(object)) { map.set(key as K, value as V); } return map; } /** * Helper function to convert an object to a reverse map. * * Note that the converted map will only have string keys, due to the nature of JavaScript objects * only having string keys under the hood. */ export function objectToReverseMap< K extends PropertyKey, V extends PropertyKey, >(object: Record): ReadonlyMap { const map = new Map(); for (const [key, value] of Object.entries(object)) { map.set(value as V, key as K); } return map; } /** * Helper function to reverse the keys and values of an object. * * @throws If the object has duplicate values. */ export function reverseObject( obj: Record, ): ReadonlyRecord { const reversed: Partial> = {}; for (const [key, value] of Object.entries(obj)) { // Type assertions are required because `Object.entries` widens the key type to a string. reversed[value as V] = key as K; } return reversed as ReadonlyRecord; }