Skip to content

Commit

Permalink
fix: ensure mapValues and group work together (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson authored Jun 26, 2024
1 parent f0e06ba commit 630f9ef
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 5 deletions.
2 changes: 1 addition & 1 deletion src/array/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
export const group = <T, Key extends string | number | symbol>(
array: readonly T[],
getGroupId: (item: T) => Key
): Partial<Record<Key, T[]>> => {
): { [K in Key]?: T[] } => {
return array.reduce((acc, item) => {
const groupId = getGroupId(item)
if (!acc[groupId]) acc[groupId] = []
Expand Down
25 changes: 25 additions & 0 deletions src/array/tests/group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,29 @@ describe('group function', () => {
expect(groups.c?.length).toBe(1)
expect(groups.c?.[0].word).toBe('ok')
})
test('works with mapValues', () => {
const objects = [
{ id: 1, group: 'a' },
{ id: 2, group: 'b' },
{ id: 3, group: 'a' }
] as const

// Notice how the types of `groupedObjects` and `groupedIds` are
// both partial (in other words, their properties have the `?:`
// modifier). At the type level, mapValues is respectful of
// preserving the partiality of the input.
const groupedObjects = _.group(objects, obj => obj.group)
const groupedIds = _.mapValues(groupedObjects, array => {
// Importantly, we can map the array without optional chaining,
// because of how the overloads of mapValues are defined.
// TypeScript knows that when a key is defined inside the result
// of a `group(…)` call, its value is never undefined.
return array.map(obj => obj.id)
})

expect(groupedIds).toEqual({
a: [1, 3],
b: [2]
})
})
})
30 changes: 26 additions & 4 deletions src/object/mapValues.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
/**
* Map over all the keys to create a new object
*/
export const mapValues = <
export function mapValues<
TValue,
TKey extends string | number | symbol,
TNewValue
>(
obj: Record<TKey, TValue>,
obj: { [K in TKey]: TValue },
mapFunc: (value: TValue, key: TKey) => TNewValue
): Record<TKey, TNewValue> => {
): { [K in TKey]: TNewValue }

// This overload exists to support cases where `obj` is a partial
// object whose values are never undefined when the key is also
// defined. For example:
// { [key: string]?: number } versus { [key: string]: number | undefined }
export function mapValues<
TValue,
TKey extends string | number | symbol,
TNewValue
>(
obj: { [K in TKey]?: TValue },
mapFunc: (value: TValue, key: TKey) => TNewValue
): { [K in TKey]?: TNewValue }

export function mapValues<
TValue,
TKey extends string | number | symbol,
TNewValue
>(
obj: { [K in TKey]?: TValue },
mapFunc: (value: TValue, key: TKey) => TNewValue
): Record<TKey, TNewValue> {
const keys = Object.keys(obj) as TKey[]
return keys.reduce((acc, key) => {
acc[key] = mapFunc(obj[key], key)
acc[key] = mapFunc(obj[key]!, key)
return acc
}, {} as Record<TKey, TNewValue>)
}
11 changes: 11 additions & 0 deletions src/object/tests/mapValues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@ describe('mapValues function', () => {
y: 'xbye'
})
})
test('objects with possibly undefined values', () => {
const result = _.mapValues({ x: 'hi ', y: undefined }, value => {
// Importantly, the value is typed as "string | undefined"
// here, due to how the overloads of mapValues are defined.
return value?.trim()
})
expect(result).toEqual({
x: 'hi',
y: undefined
})
})
})

0 comments on commit 630f9ef

Please sign in to comment.