UNPKG

@tldraw/utils

Version:

tldraw infinite canvas SDK (private utilities).

369 lines (354 loc) • 12.4 kB
import isEqualWith from 'lodash.isequalwith' /** * Safely checks if an object has a specific property as its own property (not inherited). * Uses Object.prototype.hasOwnProperty.call to avoid issues with objects that have null prototype * or have overridden the hasOwnProperty method. * * @param obj - The object to check * @param key - The property key to check for * @returns True if the object has the property as its own property, false otherwise * @example * ```ts * const obj = { name: 'Alice', age: 30 } * hasOwnProperty(obj, 'name') // true * hasOwnProperty(obj, 'toString') // false (inherited) * hasOwnProperty(obj, 'unknown') // false * ``` * @internal */ export function hasOwnProperty(obj: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key) } /** * Safely gets an object's own property value (not inherited). Returns undefined if the property * doesn't exist as an own property. Provides type-safe access with proper TypeScript inference. * * @param obj - The object to get the property from * @param key - The property key to retrieve * @returns The property value if it exists as an own property, undefined otherwise * @example * ```ts * const user = { name: 'Alice', age: 30 } * const name = getOwnProperty(user, 'name') // 'Alice' * const missing = getOwnProperty(user, 'unknown') // undefined * const inherited = getOwnProperty(user, 'toString') // undefined (inherited) * ``` * @internal */ export function getOwnProperty<K extends string, V>( obj: Partial<Record<K, V>>, key: K ): V | undefined /** @internal */ export function getOwnProperty<O extends object>(obj: O, key: string): O[keyof O] | undefined /** @internal */ export function getOwnProperty(obj: object, key: string): unknown /** @internal */ export function getOwnProperty(obj: object, key: string): unknown { if (!hasOwnProperty(obj, key)) { return undefined } // @ts-expect-error we know the property exists return obj[key] } /** * An alias for `Object.keys` that treats the object as a map and so preserves the type of the keys. * Unlike standard Object.keys which returns string[], this maintains the specific string literal types. * * @param object - The object to get keys from * @returns Array of keys with preserved string literal types * @example * ```ts * const config = { theme: 'dark', lang: 'en' } as const * const keys = objectMapKeys(config) * // keys is Array<'theme' | 'lang'> instead of string[] * ``` * @internal */ export function objectMapKeys<Key extends string>(object: { readonly [K in Key]: unknown }): Array<Key> { return Object.keys(object) as Key[] } /** * An alias for `Object.values` that treats the object as a map and so preserves the type of the * values. Unlike standard Object.values which returns unknown[], this maintains the specific value types. * * @param object - The object to get values from * @returns Array of values with preserved types * @example * ```ts * const scores = { alice: 85, bob: 92, charlie: 78 } * const values = objectMapValues(scores) * // values is Array<number> instead of unknown[] * ``` * @internal */ export function objectMapValues<Key extends string, Value>(object: { [K in Key]: Value }): Array<Value> { return Object.values(object) as Value[] } /** * An alias for `Object.entries` that treats the object as a map and so preserves the type of the * keys and values. Unlike standard Object.entries which returns `Array<[string, unknown]>`, this maintains specific types. * * @param object - The object to get entries from * @returns Array of key-value pairs with preserved types * @example * ```ts * const user = { name: 'Alice', age: 30 } * const entries = objectMapEntries(user) * // entries is Array<['name' | 'age', string | number]> * ``` * @internal */ export function objectMapEntries<Obj extends object>( object: Obj ): Array<[keyof Obj, Obj[keyof Obj]]> { return Object.entries(object) as [keyof Obj, Obj[keyof Obj]][] } /** * Returns the entries of an object as an iterable iterator. * Useful when working with large collections, to avoid allocating an array. * Only yields own properties (not inherited ones). * * @param object - The object to iterate over * @returns Iterator yielding key-value pairs with preserved types * @example * ```ts * const largeMap = { a: 1, b: 2, c: 3 } // Imagine thousands of entries * for (const [key, value] of objectMapEntriesIterable(largeMap)) { * // Process entries one at a time without creating a large array * console.log(key, value) * } * ``` * @internal */ export function* objectMapEntriesIterable<Key extends string, Value>(object: { [K in Key]: Value }): IterableIterator<[Key, Value]> { for (const key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) continue yield [key, object[key]] } } /** * An alias for `Object.fromEntries` that treats the object as a map and so preserves the type of the * keys and values. Creates an object from key-value pairs with proper TypeScript typing. * * @param entries - Array of key-value pairs to convert to an object * @returns Object with preserved key and value types * @example * ```ts * const pairs: Array<['name' | 'age', string | number]> = [['name', 'Alice'], ['age', 30]] * const obj = objectMapFromEntries(pairs) * // obj is { name: string | number, age: string | number } * ``` * @internal */ export function objectMapFromEntries<Key extends string, Value>( entries: ReadonlyArray<readonly [Key, Value]> ): { [K in Key]: Value } { return Object.fromEntries(entries) as { [K in Key]: Value } } /** * Filters an object using a predicate function, returning a new object with only the entries * that pass the predicate. Optimized to return the original object if no changes are needed. * * @param object - The object to filter * @param predicate - Function that tests each key-value pair * @returns A new object with only the entries that pass the predicate, or the original object if unchanged * @example * ```ts * const scores = { alice: 85, bob: 92, charlie: 78 } * const passing = filterEntries(scores, (name, score) => score >= 80) * // { alice: 85, bob: 92 } * ``` * @internal */ export function filterEntries<Key extends string, Value>( object: { [K in Key]: Value }, predicate: (key: Key, value: Value) => boolean ): { [K in Key]: Value } { const result: { [K in Key]?: Value } = {} let didChange = false for (const [key, value] of objectMapEntries(object)) { if (predicate(key, value)) { result[key] = value } else { didChange = true } } return didChange ? (result as { [K in Key]: Value }) : object } /** * Maps the values of an object to new values using a mapper function, preserving keys. * The mapper function receives both the key and value for each entry. * * @param object - The object whose values to transform * @param mapper - Function that transforms each value (receives key and value) * @returns A new object with the same keys but transformed values * @example * ```ts * const prices = { apple: 1.50, banana: 0.75, orange: 2.00 } * const withTax = mapObjectMapValues(prices, (fruit, price) => price * 1.08) * // { apple: 1.62, banana: 0.81, orange: 2.16 } * ``` * @internal */ export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>( object: { readonly [K in Key]: ValueBefore }, mapper: (key: Key, value: ValueBefore) => ValueAfter ): { [K in Key]: ValueAfter } { const result = {} as { [K in Key]: ValueAfter } for (const key in object) { if (!Object.prototype.hasOwnProperty.call(object, key)) continue result[key] = mapper(key, object[key]) } return result } /** * Performs a shallow equality check between two objects. Compares all enumerable own properties * using Object.is for value comparison. Returns true if both objects have the same keys and values. * * @param obj1 - First object to compare * @param obj2 - Second object to compare * @returns True if objects are shallow equal, false otherwise * @example * ```ts * const a = { x: 1, y: 2 } * const b = { x: 1, y: 2 } * const c = { x: 1, y: 3 } * areObjectsShallowEqual(a, b) // true * areObjectsShallowEqual(a, c) // false * areObjectsShallowEqual(a, a) // true (same reference) * ``` * @internal */ export function areObjectsShallowEqual<T extends object>(obj1: T, obj2: T): boolean { if (obj1 === obj2) return true const keys1 = new Set(Object.keys(obj1)) const keys2 = new Set(Object.keys(obj2)) if (keys1.size !== keys2.size) return false for (const key of keys1) { if (!keys2.has(key)) return false if (!Object.is((obj1 as any)[key], (obj2 as any)[key])) return false } return true } /** * Groups an array of values into a record by a key extracted from each value. * The key selector function is called for each element to determine the grouping key. * * @param array - The array to group * @param keySelector - Function that extracts the grouping key from each value * @returns A record where keys are the extracted keys and values are arrays of grouped items * @example * ```ts * const people = [ * { name: 'Alice', age: 25 }, * { name: 'Bob', age: 30 }, * { name: 'Charlie', age: 25 } * ] * const byAge = groupBy(people, person => `age-${person.age}`) * // { 'age-25': [Alice, Charlie], 'age-30': [Bob] } * ``` * @internal */ export function groupBy<K extends string, V>( array: ReadonlyArray<V>, keySelector: (value: V) => K ): Record<K, V[]> { const result: Record<K, V[]> = {} as any for (const value of array) { const key = keySelector(value) if (!result[key]) result[key] = [] result[key].push(value) } return result } /** * Creates a new object with specified keys omitted from the original object. * Uses shallow copying and then deletes the unwanted keys. * * @param obj - The source object * @param keys - Array of key names to omit from the result * @returns A new object without the specified keys * @example * ```ts * const user = { id: '123', name: 'Alice', password: 'secret', email: 'alice@example.com' } * const publicUser = omit(user, ['password']) * // { id: '123', name: 'Alice', email: 'alice@example.com' } * ``` * @internal */ export function omit( obj: Record<string, unknown>, keys: ReadonlyArray<string> ): Record<string, unknown> { const result = { ...obj } for (const key of keys) { delete result[key] } return result } /** * Compares two objects and returns an array of keys where the values differ. * Uses Object.is for comparison, which handles NaN and -0/+0 correctly. * Only checks keys present in the first object. * * @param obj1 - The first object (keys to check come from this object) * @param obj2 - The second object to compare against * @returns Array of keys where values differ between the objects * @example * ```ts * const before = { name: 'Alice', age: 25, city: 'NYC' } * const after = { name: 'Alice', age: 26, city: 'NYC' } * const changed = getChangedKeys(before, after) * // ['age'] * ``` * @internal */ export function getChangedKeys<T extends object>(obj1: T, obj2: T): (keyof T)[] { const result: (keyof T)[] = [] for (const key in obj1) { if (!Object.is(obj1[key], obj2[key])) { result.push(key) } } return result } /** * Deep equality comparison that allows for floating-point precision errors. * Numbers are considered equal if they differ by less than the threshold. * Uses lodash.isequalwith internally for the deep comparison logic. * * @param obj1 - First object to compare * @param obj2 - Second object to compare * @param threshold - Maximum difference allowed between numbers (default: 0.000001) * @returns True if objects are deeply equal with floating-point tolerance * @example * ```ts * const a = { x: 0.1 + 0.2 } // 0.30000000000000004 * const b = { x: 0.3 } * isEqualAllowingForFloatingPointErrors(a, b) // true * * const c = { coords: [1.0000001, 2.0000001] } * const d = { coords: [1.0000002, 2.0000002] } * isEqualAllowingForFloatingPointErrors(c, d) // true * ``` * @internal */ export function isEqualAllowingForFloatingPointErrors( obj1: object, obj2: object, threshold = 0.000001 ): boolean { return isEqualWith(obj1, obj2, (value1, value2) => { if (typeof value1 === 'number' && typeof value2 === 'number') { return Math.abs(value1 - value2) < threshold } return undefined }) }