Skip to content

Commit c8c783d

Browse files
committed
Fix: Pass original input object as mapper third argument (shallow and deep)
Fixes #44
1 parent fcdf14b commit c8c783d

5 files changed

Lines changed: 60 additions & 16 deletions

File tree

index.d.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ console.log(result);
1616
export const mapObjectSkip: unique symbol;
1717

1818
export type Mapper<
19-
SourceObjectType extends Record<string, any>,
19+
SourceObjectType extends Record<string, unknown>,
2020
MappedObjectKeyType extends string,
2121
MappedObjectValueType,
2222
> = (
@@ -34,13 +34,14 @@ Mapper used when `{deep: true}` is enabled.
3434
3535
In deep mode we may visit nested objects with keys and values unrelated to the top-level object, so we intentionally widen the key and value types.
3636
*/
37-
export type DeepMapper<
37+
type DeepMapper<
38+
SourceObjectType extends Record<string, unknown>,
3839
MappedObjectKeyType extends string,
3940
MappedObjectValueType,
4041
> = (
4142
sourceKey: string,
42-
sourceValue: any,
43-
source: any
43+
sourceValue: unknown,
44+
source: SourceObjectType
4445
) => [
4546
targetKey: MappedObjectKeyType,
4647
targetValue: MappedObjectValueType,
@@ -60,14 +61,14 @@ export interface Options {
6061
6162
@default {}
6263
*/
63-
readonly target?: Record<string, any>;
64+
readonly target?: Record<string, unknown>;
6465
}
6566

6667
export interface DeepOptions extends Options {
6768
readonly deep: true;
6869
}
6970

70-
export interface TargetOptions<TargetObjectType extends Record<string, any>> extends Options {
71+
export interface TargetOptions<TargetObjectType extends Record<string, unknown>> extends Options {
7172
readonly target: TargetObjectType;
7273
}
7374

@@ -111,12 +112,12 @@ const newObject = mapObject({one: 1, two: 2}, (key, value) => value === 1 ? [key
111112
*/
112113
export default function mapObject<
113114
SourceObjectType extends Record<string, unknown>,
114-
TargetObjectType extends Record<string, any>,
115+
TargetObjectType extends Record<string, unknown>,
115116
MappedObjectKeyType extends string,
116117
MappedObjectValueType,
117118
>(
118119
source: SourceObjectType,
119-
mapper: DeepMapper<MappedObjectKeyType, MappedObjectValueType>,
120+
mapper: DeepMapper<SourceObjectType, MappedObjectKeyType, MappedObjectValueType>,
120121
options: DeepOptions & TargetOptions<TargetObjectType>
121122
): TargetObjectType & Record<string, unknown>;
122123
export default function mapObject<
@@ -125,12 +126,12 @@ export default function mapObject<
125126
MappedObjectValueType,
126127
>(
127128
source: SourceObjectType,
128-
mapper: DeepMapper<MappedObjectKeyType, MappedObjectValueType>,
129+
mapper: DeepMapper<SourceObjectType, MappedObjectKeyType, MappedObjectValueType>,
129130
options: DeepOptions
130131
): Record<string, unknown>;
131132
export default function mapObject<
132-
SourceObjectType extends Record<string, any>,
133-
TargetObjectType extends Record<string, any>,
133+
SourceObjectType extends Record<string, unknown>,
134+
TargetObjectType extends Record<string, unknown>,
134135
MappedObjectKeyType extends string,
135136
MappedObjectValueType,
136137
>(
@@ -143,7 +144,7 @@ export default function mapObject<
143144
options: TargetOptions<TargetObjectType>
144145
): TargetObjectType & {[K in MappedObjectKeyType]: MappedObjectValueType};
145146
export default function mapObject<
146-
SourceObjectType extends Record<string, any>,
147+
SourceObjectType extends Record<string, unknown>,
147148
MappedObjectKeyType extends string,
148149
MappedObjectValueType,
149150
>(

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,8 @@ export default function mapObject(object, mapper, options) {
6666
throw new TypeError('Expected an object, got an array');
6767
}
6868

69-
return _mapObject(object, mapper, options);
69+
// Ensure the third mapper argument is always the original input object
70+
const root = object;
71+
const mapperWithRoot = (key, value) => mapper(key, value, root);
72+
return _mapObject(object, mapperWithRoot, options);
7073
}

index.test-d.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,24 @@ expectType<{baz: string} & Record<string, 'foo'>>(object1);
2222
expectType<'foo'>(object1.bar);
2323
expectType<string>(object1.baz);
2424

25-
const object2 = mapObject({foo: 'bar'}, (key, value) => [value, key], {
25+
const object2 = mapObject({foo: 'bar'}, (key, value) => [String(value), key], {
2626
deep: true,
2727
});
2828
expectType<Record<string, unknown>>(object2);
2929
// Deep mapper parameters should be widened
30-
mapObject({fooUpper: true, bAr: {bAz: true}}, (key, value) => {
30+
mapObject({fooUpper: true, bAr: {bAz: true}}, (key, value, source) => {
3131
expectType<string>(key);
32+
// In deep mode, source is the original input object
33+
expectType<{fooUpper: boolean; bAr: {bAz: boolean}}>(source);
3234
return [String(key), value];
3335
}, {deep: true});
34-
const object3 = mapObject({foo: 'bar'}, (key, value) => [value, key], {
36+
37+
// Shallow mode: source should be the original input type
38+
mapObject({alpha: 1, beta: 2}, (key, value, source) => {
39+
expectType<{alpha: number; beta: number}>(source);
40+
return [key, value];
41+
});
42+
const object3 = mapObject({foo: 'bar'}, (key, value) => [String(value), key], {
3543
deep: true,
3644
target: {bar: 'baz' as const},
3745
});

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Type: `(sourceKey, sourceValue, source) => [targetKey, targetValue, mapperOption
4646

4747
A mapping function.
4848

49+
> [!NOTE]
50+
> When `options.deep` is `true`, the mapper can receive keys and values from nested objects and arrays. In that case, the mapper parameters are intentionally widened: `sourceKey` is typed as `string` and `sourceValue` as `unknown`, reflecting the actual runtime behavior when recursing into unknown shapes. The third argument `source` is always the original input object, not the current nested owner.
51+
4952
##### mapperOptions
5053

5154
Type: `object`

test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ test('main', t => {
77
t.is(mapObject({foo: 'bar'}, (key, value) => [value, key]).bar, 'foo');
88
});
99

10+
test('mapper source argument is the original input (shallow)', t => {
11+
const input = {foo: {bar: 1}};
12+
mapObject(input, (key, value, source) => {
13+
t.is(source, input);
14+
return [key, value];
15+
});
16+
});
17+
18+
test('mapper source argument is the original input when using target', t => {
19+
const input = {x: 1};
20+
const target = {y: 2};
21+
mapObject(input, (key, value, source) => {
22+
t.is(source, input);
23+
t.not(source, target);
24+
return [key, value];
25+
}, {target});
26+
// Ensure target still works as target
27+
t.deepEqual(target, {y: 2, x: 1});
28+
});
29+
1030
test('target option', t => {
1131
const target = {};
1232
t.is(mapObject({foo: 'bar'}, (key, value) => [value, key], {target}), target);
@@ -47,6 +67,15 @@ test('deep option', t => {
4767
t.deepEqual(actual, expected);
4868
});
4969

70+
test('mapper source arg is the original input (deep)', t => {
71+
const input = {a: {b: {c: 1}}};
72+
mapObject(input, (key, value, source) => {
73+
// Should always be the root input object
74+
t.is(source, input);
75+
return [key, value];
76+
}, {deep: true});
77+
});
78+
5079
test('shouldRecurse mapper option', t => {
5180
const object = {
5281
one: 1,

0 commit comments

Comments
 (0)