UNPKG

remeda

Version:

A utility library for JavaScript and Typescript.

1 lines 11.7 kB
{"version":3,"file":"groupByProp.cjs","names":["purry","output: BoundedPartial<Record<AllPropValues<T, Prop>, T[number][]>>"],"sources":["../src/groupByProp.ts"],"sourcesContent":["import type {\n AllUnionFields,\n ConditionalKeys,\n EmptyObject,\n IsNever,\n Or,\n Simplify,\n} from \"type-fest\";\nimport type { ArrayRequiredPrefix } from \"./internal/types/ArrayRequiredPrefix\";\nimport type { BoundedPartial } from \"./internal/types/BoundedPartial\";\nimport type { FilteredArray } from \"./internal/types/FilteredArray\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\nimport type { TupleParts } from \"./internal/types/TupleParts\";\nimport { purry } from \"./purry\";\n\ntype GroupByProp<T extends IterableContainer, Prop extends GroupableProps<T>> =\n // Distribute unions.\n T extends unknown\n ? FixEmptyObject<EnsureValuesAreNonEmpty<GroupByPropRaw<T, Prop>>>\n : never;\n\n// For each possible value of the prop we filter the input tuple with the prop\n// assigned to the value, e.g. `{ type: \"cat\" }`\ntype GroupByPropRaw<\n T extends IterableContainer,\n Prop extends GroupableProps<T>,\n> = {\n [Value in AllPropValues<T, Prop>]: FilteredArray<T, Record<Prop, Value>>;\n};\n\n// We can only group by props that only have values that could be used to key\n// an object (i.e. PropertyKey), or if they are undefined (which would filter\n// them out of the grouping).\ntype GroupableProps<T extends IterableContainer> = ConditionalKeys<\n ItemsSuperObject<T>,\n PropertyKey | undefined\n>;\n\n// The union of all possible values that the prop could have within the tuple.\ntype AllPropValues<\n T extends IterableContainer,\n Prop extends GroupableProps<T>,\n> = Extract<ItemsSuperObject<T>[Prop], PropertyKey>;\n\n// Creates a singular object type that all items in the tuple would extend. This\n// provides us a way to check, for each prop, what are all values it would\n// have within the tuple. We use this to map which props are candidates for\n// grouping, and when a prop is selected, the full list of values that would\n// exist in the output. For example:\n// `{ a: number, b: \"cat\", c: string } | { b: \"dog\", c: Date }` is groupable\n// by 'a' and 'b', but not 'c', and when selecting by 'b', the output would\n// have a prop for \"cat\" and a prop for \"dog\".\ntype ItemsSuperObject<T extends IterableContainer> = AllUnionFields<\n // If the input tuple contains optional elements they would add `undefined` to\n // T[number] (and could technically show up in the array itself). Because\n // undefined breaks AllUnionFields we need to remove it from the union. This\n // is OK because we handle this in the implementation too.\n Exclude<T[number], undefined>\n>;\n\n// When the input array is empty the constructed result type would be `{}`\n// because our mapped type would never run; but this doesn't represent the\n// semantics of the return value for that case, because it effectively means\n// \"any object\" and not \"empty object\". This can happen in 2 situations:\n// A union of tuples where one of the tuples doesn't have any item with the\n// groupable prop, or when the groupable prop has a value of `undefined` for\n// all items. The former is extra problematic because it would add `| {}` to the\n// result type which effectively cancels out all other parts of the union.\ntype FixEmptyObject<T> = IsNever<keyof T> extends true ? EmptyObject : T;\n\n// Group by can never return an empty tuple but our filtered arrays might not\n// represent that. We need to reshape the tuples so that they always have at\n// least one item in them.\ntype EnsureValuesAreNonEmpty<T extends Record<PropertyKey, IterableContainer>> =\n Simplify<\n Omit<T, PossiblyEmptyArrayKeys<T>> &\n BoundedPartial<CoercedNonEmptyValues<Pick<T, PossiblyEmptyArrayKeys<T>>>>\n >;\n\n// Go over the keys the object and return those that their value can accept an\n// empty array.\ntype PossiblyEmptyArrayKeys<T extends Record<PropertyKey, IterableContainer>> =\n keyof T extends infer Key extends unknown\n ? Key extends keyof T\n ? IsNonEmptyArray<T[Key]> extends true\n ? never\n : Key\n : never\n : never;\n\n// An array is non-empty if any of the fixed parts are non-empty.\ntype IsNonEmptyArray<T extends IterableContainer> = Or<\n IsNonEmptyFixedTuple<TupleParts<T>[\"required\"]>,\n IsNonEmptyFixedTuple<TupleParts<T>[\"suffix\"]>\n>;\n\n// A fixed tuple (one without optional or a rest element in it) is non-empty if\n// we can't extract the empty tuple from it.\ntype IsNonEmptyFixedTuple<T> = IsNever<Extract<T, readonly []>>;\n\n// We coerce the arbitrary array values to have a prefix of at least one item.\ntype CoercedNonEmptyValues<T extends Record<PropertyKey, IterableContainer>> = {\n [P in keyof T]: ArrayRequiredPrefix<T[P], 1>;\n};\n\n/**\n * Groups the elements of an array of objects based on the values of a\n * specified property of those objects. The result would contain a property for\n * each unique value of the specific property, with it's value being the input\n * array filtered to only items that have that property set to that value.\n * For any object where the property is missing, or if it's value is\n * `undefined` the item would be filtered out.\n *\n * The grouping property is enforced at the type level to exist in at least one\n * item and to never have a value that cannot be used as an object key (e.g. it\n * must be `PropertyKey | undefined`).\n *\n * The resulting arrays are filtered with the prop and it's value as a\n * type-guard, effectively narrowing the items in each output arrays. This\n * means that when the grouping property is the discriminator of a\n * discriminated union type each output array would contain just the subtype for\n * that value.\n *\n * If you need more control over the grouping you should use `groupBy` instead.\n *\n * @param data - The items to group.\n * @param prop - The property name to group by.\n * @signature\n * R.groupByProp(data, prop)\n * @example\n * const result = R.groupByProp(\n * // ^? { cat: [{ a: 'cat' }], dog: [{ a: 'dog' }] }\n * [{ a: 'cat' }, { a: 'dog' }] as const,\n * 'a',\n * );\n * @dataFirst\n * @category Array\n */\nexport function groupByProp<\n T extends IterableContainer,\n const Prop extends GroupableProps<T>,\n>(data: T, prop: Prop): GroupByProp<T, Prop>;\n\n/**\n * Groups the elements of an array of objects based on the values of a\n * specified property of those objects. The result would contain a property for\n * each unique value of the specific property, with it's value being the input\n * array filtered to only items that have that property set to that value.\n * For any object where the property is missing, or if it's value is\n * `undefined` the item would be filtered out.\n *\n * The grouping property is enforced at the type level to exist in at least one\n * item and to never have a value that cannot be used as an object key (e.g. it\n * must be `PropertyKey | undefined`).\n *\n * The resulting arrays are filtered with the prop and it's value as a\n * type-guard, effectively narrowing the items in each output arrays. This\n * means that when the grouping property is the discriminator of a\n * discriminated union type each output array would contain just the subtype for\n * that value.\n *\n * If you need more control over the grouping you should use `groupBy` instead.\n *\n * @param prop - The property name to group by.\n * @signature\n * R.groupByProp(prop)(data);\n * @example\n * const result = R.pipe(\n * // ^? { cat: [{ a: 'cat' }], dog: [{ a: 'dog' }] }\n * [{ a: 'cat' }, { a: 'dog' }] as const,\n * R.groupByProp('a'),\n * );\n * @dataLast\n * @category Array\n */\nexport function groupByProp<\n T extends IterableContainer,\n const Prop extends GroupableProps<T>,\n>(prop: Prop): (data: T) => GroupByProp<T, Prop>;\n\nexport function groupByProp(...args: readonly unknown[]): unknown {\n return purry(groupByPropImplementation, args);\n}\n\nfunction groupByPropImplementation<\n T extends IterableContainer,\n Prop extends GroupableProps<T>,\n>(data: T, prop: Prop): GroupByProp<T, Prop> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Using Object.create(null) allows us to remove everything from the prototype chain, leaving it as a pure object that only has the keys *we* add to it. This prevents issues like the one raised in #1046\n const output: BoundedPartial<Record<AllPropValues<T, Prop>, T[number][]>> =\n Object.create(null);\n\n for (const item of data) {\n const key = item?.[prop];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is incorrect and a result of TypeScript not inferring the type of `key` correctly. It stems from a chain of bad inferences which start with `item` being inferred eagerly as `unknown` (because of: https://github.com/microsoft/TypeScript/issues/61750). When accessing a prop on `unknown` TypeScript then infers the result as `{}[Prop] | undefined`. What follows is that `{}[Prop]` is inferred as `never` (because we are accessing an object with no props defined on it), leading the union to simplify to just `undefined`. The correct type should have been `AllPropValues<T, Prop> | undefined`.\n if (key !== undefined) {\n // Once the prototype chain is fixed, it is safe to access the prop\n // directly without needing to check existence or types.\n const items = output[key];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Following the problem above, TypeScript then thinks that `key` is `never`, and also types `items` as `never`.\n if (items === undefined) {\n // It is more performant to create a 1-element array over creating an\n // empty array and falling through to a unified the push. It is also\n // more performant to mutate the existing object over using spread to\n // continually create new objects on every unique key.\n // @ts-expect-error [ts7053] -- For the same reasons as mentioned above, TypeScript isn't inferring `key` correctly, and therefore is erroring when trying to access the output object using it.\n output[key] = [item];\n } else {\n // It is more performant to add the items to an existing array instead\n // of creating a new array via spreading every time we add an item to\n // it (e.g., `[...current, item]`).\n // @ts-expect-error [ts2339] -- And again here `items` is still `never`.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- See above.\n items.push(item);\n }\n }\n }\n\n // Set the prototype as if we initialized our object as a normal object (e.g.\n // `{}`). Without this none of the built-in object methods like `toString`\n // would work on this object and it would act differently than expected.\n Object.setPrototypeOf(output, Object.prototype);\n\n // @ts-expect-error [ts2322] -- This is fine! We use a broader type for output while we build it because it more accurately represents the shape of the object *while it is being built*. TypeScript can't tell that we finished building the object so can't ensure that output matches the expected output at this point.\n return output;\n}\n"],"mappings":"wCAoLA,SAAgB,EAAY,GAAG,EAAmC,CAChE,OAAOA,EAAAA,EAAM,EAA2B,EAAK,CAG/C,SAAS,EAGP,EAAS,EAAkC,CAE3C,IAAMC,EACJ,OAAO,OAAO,KAAK,CAErB,IAAK,IAAM,KAAQ,EAAM,CACvB,IAAM,EAAM,IAAO,GAEnB,GAAI,IAAQ,IAAA,GAAW,CAGrB,IAAM,EAAQ,EAAO,GAGjB,IAAU,IAAA,GAMZ,EAAO,GAAO,CAAC,EAAK,CAOpB,EAAM,KAAK,EAAK,EAWtB,OAHA,OAAO,eAAe,EAAQ,OAAO,UAAU,CAGxC"}