import type {IsTuple, Simplify} from 'type-fest'; import type {ArrayOwnKeys, ArrayEntryKey, ArrayEntryValue} from './internal-types.js'; type OptionalMappedKeys = Simplify<{-readonly [Key in Keys as `${Extract}`]?: NewValue}>; type MappedArrayOwnValues = OptionalMappedKeys, NewValue>; type HasTupleIndex = Index extends ArrayOwnKeys ? true : `${Index}` extends ArrayOwnKeys ? true : false; // Fixed tuples need their own index-key walk because `[...Type]` widens away the exact tuple slots we still want to preserve. type FixedTupleIndexKeys = HasTupleIndex extends true ? FixedTupleIndexKeys : Keys; type TupleStaticPart = number extends Type['length'] ? Type extends readonly [infer Head, ...infer Tail] ? TupleStaticPart : Result : Type; type TupleExtraKeys = number extends Type['length'] ? VariadicTupleExtraKeys : Extract, FixedTupleIndexKeys>, keyof Type>; // Numeric tuple extra keys are the explicit widening boundary for this API. // Once tuple extras look like array indices, we stop promising tuple shape and fall back to array results. type NumericTupleExtraKeys = Extract, number | `${number}`>; type MappedPlainTupleElement = number extends Extract ? NewValue : {} extends Pick ? NewValue | undefined : NewValue; type MappedPlainTupleValues = {-readonly [Key in keyof Type]: MappedPlainTupleElement}; type NumericLiteralKeys = keyof { [Key in Extract, number> as number extends Key ? never : Key]: unknown; }; type VariadicTupleTupleKeys = FixedTupleIndexKeys> | NumericLiteralKeys; type VariadicTupleExtraKeys = Extract | keyof unknown[] | 'length'> | Exclude, VariadicTupleTupleKeys>, keyof Type>; type MappedTupleElement = {} extends Pick ? NewValue | undefined : NewValue; type MappedTupleIntersectionElements = HasTupleIndex extends true ? {} extends Pick ? Mapped | MappedTupleIntersectionElements]> : MappedTupleIntersectionElements]> : Mapped; type MappedFixedTupleBaseValues = TupleExtraKeys extends never ? undefined extends Type[number] ? MappedTupleIntersectionElements : MappedPlainTupleValues : MappedTupleIntersectionElements; type MappedTupleBaseValues = number extends Type['length'] // Plain variadic tuples can keep their full mapped shape, including required suffix slots after the rest element. ? TupleExtraKeys extends never ? MappedPlainTupleValues : [...MappedPlainTupleValues, NewValue>, ...NewValue[]] : MappedFixedTupleBaseValues; // Tuples preserve their base shape plus named extra keys. Numeric extra keys are the explicit widening boundary. // Once we widen to array results, we intentionally stop modeling sparse holes from positive numeric extra keys. // This helper treats sparse-array precision as unsupported and only preserves the value/index shape at that point. type MappedTupleValues = TupleExtraKeys extends never ? MappedTupleBaseValues : NumericTupleExtraKeys extends never ? MappedTupleBaseValues & OptionalMappedKeys, NewValue> : NewValue[] & OptionalMappedKeys, NewValue>; // Non-tuple arrays use dense array typing on purpose. // Sparse holes and far numeric writes are runtime behavior that this helper intentionally does not model precisely. type MappedArrayValues = IsTuple extends true ? MappedTupleValues : NewValue[] & MappedArrayOwnValues; type IndexedCollectionElement = Type extends ArrayLike ? Element : never; type IndexedCollectionMappedValues = Partial>; type IsTypedArrayOrBuffer = Type extends readonly unknown[] ? false : Type extends ArrayBufferView ? Type extends DataView ? false : true : false; type NormalizeStrict = IsStrict extends false ? false : true; type ObjectSourceKeys = Extract; type HasFunctionValues = Extract], (...arguments_: never[]) => unknown> extends never ? false : true; type HasLooseObjectCallbackShape = Type extends Record ? HasFunctionValues extends true ? false : true : false; type LooseObjectCallbackKey = Type extends unknown ? HasLooseObjectCallbackShape extends true ? `${ObjectSourceKeys}` : string : never; type StrictObjectCallbackKey = Type extends unknown ? Type extends Record ? `${ObjectSourceKeys}` : string : never; type LooseObjectCallbackValue = Type extends unknown ? HasLooseObjectCallbackShape extends true ? Type[ObjectSourceKeys] : unknown : never; type StrictObjectCallbackValue = Type extends unknown ? Type extends Record ? Type[ObjectSourceKeys] : unknown : never; type StrictObjectMappedValues = Type extends Record ? Simplify}`]: NewValue}>> : Partial>; // Non-array object results stay conservative in both modes because TypeScript cannot prove own-enumerable properties from static shape alone. // Loose mode only switches callbacks to the data-object path for shapes without function-valued members. type ObjectMappedValues = StrictObjectMappedValues; type MapFunctionKey = Type extends unknown ? Type extends readonly unknown[] ? ArrayEntryKey : IsTypedArrayOrBuffer extends true ? `${number}` : IsStrict extends true ? StrictObjectCallbackKey : LooseObjectCallbackKey : never; type MapFunctionValue = Type extends unknown ? Type extends readonly unknown[] ? ArrayEntryValue : IsTypedArrayOrBuffer extends true ? IndexedCollectionElement : IsStrict extends true ? StrictObjectCallbackValue : LooseObjectCallbackValue : never; type MappedValues = Type extends readonly unknown[] ? MappedArrayValues : IsTypedArrayOrBuffer extends true ? IndexedCollectionMappedValues : ObjectMappedValues; /** A strongly-typed version of mapping over an object's values, preserving keys. This avoids the common footgun where `objectFromEntries(objectKeys(obj).map(…))` produces optional keys because `.map()` returns a dynamic-length array. For non-array objects, strict mode is the default and keeps the result conservative: declared keys become optional because TypeScript cannot know whether a property is an own enumerable property from static shape alone. Pass `{strict: false}` to use the data-object callback path for plain shapes without function-valued members. The non-array object result still stays conservative in both modes, including for interface, class, and structural aliases, because `Object.entries()` only returns own enumerable properties. Array inputs follow `Object.entries()` semantics for elements: only present enumerable elements are mapped, so sparse holes are skipped at runtime. For non-tuple arrays, the types intentionally use a dense array model and do not try to represent sparse holes or far numeric writes precisely; only numeric entries are reflected in the types, and named custom properties on arrays are treated as unsupported. For tuple inputs, named extra keys are preserved when they do not shadow existing array members, but they stay optional in the result because static types cannot prove those properties are enumerable. Numeric extra keys are a widening boundary and produce array results instead of tuple-preserving ones. Typed arrays and `Buffer` are typed as numeric indexed collections too, but their mapped result stays a plain object with numeric keys because that matches the current runtime implementation. @example ``` import {objectMapValues} from 'ts-extras'; const object = {a: 1, b: 2, c: 3}; const mapped = objectMapValues(object, value => String(value)); //=> {a?: string; b?: string; c?: string} objectMapValues(object, (value, key) => { value satisfies 1 | 2 | 3; key satisfies 'a' | 'b' | 'c'; return String(value); }, {strict: false}); ``` @category Improved builtin */ export function objectMapValues( object: Type, mapFunction: (value: MapFunctionValue>, key: MapFunctionKey>) => NewValue, options?: {strict?: IsStrict}, ): MappedValues>; export function objectMapValues( object: any, // eslint-disable-line @typescript-eslint/no-explicit-any mapFunction: (value: any, key: string) => any, // eslint-disable-line @typescript-eslint/no-explicit-any options?: {strict?: boolean}, ): any { // eslint-disable-line @typescript-eslint/no-explicit-any void options; const result: Record | unknown[] = Array.isArray(object) ? [] : Object.getPrototypeOf(object) === null ? Object.create(null) : {}; // Intentionally follow `Object.entries()` key discovery semantics. // Do not walk prototypes or inspect property descriptors here. for (const [key, value] of Object.entries(object)) { Object.defineProperty(result, key, { value: mapFunction(value, key), enumerable: true, writable: true, configurable: true, }); } return result; }