Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/improve group by key types #143

Merged
merged 16 commits into from Nov 11, 2021

Conversation

TkDodo
Copy link
Collaborator

@TkDodo TkDodo commented Aug 13, 2021

by inferring the type of the key from the provided function; it must still extend a PropertyKey because that's what an object needs as index

@Bessonov
Copy link

Bessonov commented Nov 8, 2021

@TkDodo Wow, great! I had the same idea after wondering about groupBy return types. From my point of view, you need to include undefined:

): Record<PropertyKey, NonEmptyArray<T> | undefined>;

But I'm not sure about this change: microsoft/TypeScript#13195 . It could lead to different semantic.

@TkDodo
Copy link
Collaborator Author

TkDodo commented Nov 8, 2021

@Bessonov why would we need to include undefined? After we group, the resulting object will only have keys where also values exist for it. That's also the reason why we have a NonEmptyArray and not just an Array - because accessing the first element is safe, as it will always be there...

@Bessonov
Copy link

Bessonov commented Nov 8, 2021

@TkDodo correct me, if I'm wrong, but without undefined you get no type error here:

interface MyType {
	type: 'right' | 'wrong'
}

const myTypes: MyType[] = [
	{ type: 'right' }
]

const grouped = groupBy(myTypes, item => item.type)

grouped.wrong.length <-- fine

But it breaks at runtime.

@Bessonov
Copy link

Bessonov commented Nov 8, 2021

Or, I think, using Partial would preserve the semantic:

Partial<Record<PropertyKey, NonEmptyArray<T>>>

@TkDodo
Copy link
Collaborator Author

TkDodo commented Nov 9, 2021

@Bessonov you are most certainly right 👍

However, adding undefined to the mix will make it harder to work with when iterating over the object with Object.entries. consider:

interface MyType {
	type: 'right' | 'wrong'
}

const myTypes: MyType[] = [
	{ type: 'right' }
]

const grouped = groupBy(myTypes, item => item.type)

Object.entries(grouped).forEach(([, value]) => {
    console.log(value[0])
})

Here, we should be able to access value[0], but this won't work if we add undefined to the mix.

Maybe what I'm trying to do in this PR is not the right thing to do? After all, I think things like this are the reason why TypeScript only returns string for Object.keys ...

@Bessonov
Copy link

Bessonov commented Nov 9, 2021

@TkDodo I've created a playground with all three definitions above. It looks like the "Partial"-Definition

declare function groupByPartial<Item, Key extends PropertyKey>(
  items: readonly Item[], fn: (item: Item) => Key,
): Partial<Record<Key, NonEmptyArray<Item>>>

works as expected. What do you think about that?

However, adding undefined to the mix will make it harder to work with when iterating over the object with Object.entries. consider:

If I must choose between groupByOriginal and groupByUndefined, then I would choose groupByUndefined, because in this case I think that it's a bug of Object.entries and in my practice groupByUndefined gives more benefit. But I hope that groupByPartial works as expected.

@TkDodo
Copy link
Collaborator Author

TkDodo commented Nov 10, 2021

@Bessonov thanks for digging into this. You are right that the Partial solution works - with TS 4.3 and onwards. I was just double checking why it doesn't work when I wrote a test for it, and it seems to be that we are on TS 4.0 still in the repo 😅 .

I will update the PR to use Partial and also update the TS version to the latest.

reverse type inference gets even better with this
we can't really access keys directly because they might not exist at runtime, and Partial makes sure of that while still preserving the better key types. this also works well with Object.entries in TS 4.3 and above
@TkDodo
Copy link
Collaborator Author

TkDodo commented Nov 10, 2021

@Bessonov have another look please

Copy link

@Bessonov Bessonov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!

src/groupBy.ts Outdated
export function indexed<Item, Key extends PropertyKey>(
array: readonly Item[],
fn: PredIndexed<Item, Key>
): Record<Key, NonEmptyArray<Item>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not using the indexed version, but shouldn't it have same return type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx for seeing that, you are right 🙏

b66c416

const grouped = groupBy(myTypes, item => item.type);

// @ts-expect-error we can't access keys unconditionally
expect(() => grouped.wrong.length).toThrow();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add

    // @ts-expect-error other values are not allowed by typings
   expect(grouped.notexists).toBeUndefined();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

items: readonly T[],
fn: (item: T) => PropertyKey
): Record<PropertyKey, NonEmptyArray<T>>;
export function groupBy<Item, Key extends PropertyKey>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but I think we can replace this definition one day:

/** types that may be returned by `keyof` */
export type Key = string | number | symbol;

with PropertyKey.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done: 83e04aa

@Bessonov
Copy link

Great, I'm fine with it, thank you!

@TkDodo TkDodo merged commit 9ea42bc into remeda:master Nov 11, 2021
@TkDodo TkDodo deleted the feature/improve-groupBy-key-types branch November 11, 2021 07:58
@nghiepdev nghiepdev mentioned this pull request Dec 2, 2021
TkDodo added a commit that referenced this pull request Dec 3, 2021
vlad-iakovlev pushed a commit to adrgautier/remeda that referenced this pull request Jul 29, 2022
* improve groupBy key typings

by inferring the type of the key from the provided function; it must still extend a PropertyKey because that's what an object needs as index

* update typescript version

reverse type inference gets even better with this

* make returnType of groupBy partial

we can't really access keys directly because they might not exist at runtime, and Partial makes sure of that while still preserving the better key types. this also works well with Object.entries in TS 4.3 and above

* add an extra test for unknown properties

* use built-in PropertyKey instead of our own type

* fix types for indexed version
vlad-iakovlev pushed a commit to adrgautier/remeda that referenced this pull request Jul 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants