plexinc/papr

Wrong ProjectionType when suppressing _id field from the projection

Closed this issue · 6 comments

Hi!

The ProjectionType util doesn't support the suppression of _id field from the projection.

Example:

const projection = {
  _id: 0,
  firstName: 1
}

type UserProjected = ProjectionType<UserDocument, typeof projection>;
// UserProjected type is { _id: ObjectId, firstName: string}
// However, UserProjected type should be  {firstName: string}

Thanks!

That's because the ProjectionType util returns a WithId generic.

papr/src/utils.ts

Lines 102 to 109 in 0c3d992

export type ProjectionType<
TSchema extends BaseSchema,
Projection extends
| Partial<Record<Join<NestedPaths<WithId<TSchema>, []>, '.'>, number>>
| undefined
> = undefined extends Projection
? WithId<TSchema>
: WithId<DeepPick<TSchema, '_id' | (keyof Projection & string)>>;

It would be very complex to detect if you want to suppress _id with _id: 0 so they probably decided to always return it on the type and I think it's very unusual for your to decide to not project the _id since you can just omit it before returning it. My suggestion would be to just omit it on the UserProjected using Omit<ProjectionType<UserDocument, typeof projection>, "_id"> but maybe that's open for discussion.

Let me write here one experiment I was doing yesterday


type User = {
  _id: number;
  firstName: number;
};

type GetProjection<
  Schema,
  Projection extends Record<string, 0 | 1> | undefined,
> = undefined extends Projection
  ? 'undefined'
  : Projection extends { _id: 0 }
  ? 'withoutId'
  : 'withId';

const projection1 = {
  _id: 0,
  firstName: 1,
} as const;

const projection2 = {
  _id: 1,
  firstName: 1,
} as const;

type UserProjected1 = GetProjection<User, typeof projection1>; // type is 'withoutId'

type UserProjected2 = GetProjection<User, typeof projection2>; // type is 'withId'

type UserProjected3 = GetProjection<User, undefined>; // type is 'undefined'

I was trying to substitute the strings 'undefined', 'withId', 'withoutId' by the real types, but it was not working to me. Maybe we could try to figure out what is failing with this approach when using the real types. Any idea?

Check if this works for you:

export type ProjectionType<
  TSchema extends BaseSchema,
  Projection extends
    | Partial<Record<Join<NestedPaths<WithId<TSchema>, []>, ".">, number>>
    | undefined
> = undefined extends Projection
  ? WithId<TSchema>
  : Projection extends { _id: 0 }
  ? Omit<DeepPick<TSchema, "_id" | (keyof Projection & string)>, "_id">
  : WithId<DeepPick<TSchema, "_id" | (keyof Projection & string)>>;

const projection = {
  _id: 0,
  firstName: 1,
} as const;

type UserProjected = ProjectionType<UserDocument, typeof projection>;

Yes, that's working.

I was trying things like

export type ProjectionType<
  TSchema extends BaseSchema,
  Projection extends
    | Partial<Record<Join<NestedPaths<WithId<TSchema>, []>, ".">, number>>
    | undefined
> = undefined extends Projection
  ? WithId<TSchema>
  : Projection extends { _id: 0 }
  ? DeepPick<TSchema,(keyof Projection & string)>
  : WithId<DeepPick<TSchema, "_id" | (keyof Projection & string)>>;

without success.

But your approach it's working and it's simple.
Thanks!

@avaly what do you think about the proposed solution?

avaly commented

Hi folks!

It's truly wonderful for our team to see so many folks taking an interest in our project in the first place.

I would approve a PR with the proposed changes in this GHI. If no one has time to open one, I'll get around to making this change once my schedule allows it.

Thanks!

@avaly I've put some tests and modified the documentation.

I think the code is working perfectly. However, there is a breaking change when using a projection.

With the new behaviour we need to identify if we are including or excluding (1 or 0) one field in the projection. Therefore, we must use as const to get the correct type.