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

feat: depth generic, fully type safe result types for relationships and joins #9782

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

r1tsuu
Copy link
Member

@r1tsuu r1tsuu commented Dec 6, 2024

This feature allows you to have fully type safe results for relationship / joins fields depending on depth. Currently this is opt-in with typescript.typeSafeDepth: true property, as it may break existing types. Meaning without enabling it - there's no any effect on your existing project. Discussion - #8229

For example, with the given config:

export default buildConfig({
  collections: [
    {
      slug: 'books',
      fields: [
         {
          type: 'relationship',
          name: 'relatedMovie',
          relationTo: 'movies',
        },
        {
          type: 'join',
          on: 'books',
          name: 'author',
          collection: 'authors',
        },
      ],
    },
    {
      slug: 'authors',
      fields: [
        {
          type: 'relationship',
          hasMany: true,
          relationTo: 'books',
          name: 'books',
        },
      ],
    },
    {
      slug: 'movies',
      fields: [
        {
          type: 'relationship',
          relationTo: 'authors',
          name: 'author',
        },
      ],
    },
  ],
  // default in Payload
  defaultDepth: 2,
  typescript: {
    typeSafeDepth: true,
  },
})

Let's try to fetch some movies using the Local API:

Without specifying depth, the result type for each movie is ApplyDepth<Movie, 2>: (From defaultDepth)
image

movie.author is ApplyDepth<Author, 1>, without this feature it'd have been annoying string | Author :
image

movie.author.books is ApplyDepth<Book, 0>[] because of the hasMany relationship:
image

But then, if we try to access movie.author.books[0].relatedMovie we get string which is exactly what we expect from depth: 2:
image
However, with depth: 3 we get relatedMovie: ApplyDepth<Movie, 0>:
image

Now, let's try to specify depth: 0:
The result type is ApplyDepth<Movie, 0> and movie.author is string:
image

This also works for join fields as well.

Challenges:

  • This works across for any nesting of your relationship fields (including polymorphic ones) to arrays / groups / blocks. To determine whether the current type is a relationship or not, we need some indicator and there's no currently one. This PR adds an internal __collection property (if typescript.typeSafeDepth for collection generated types which the generic uses.
    __collection?: 'posts';
  • There's no really an easy way to subtract number types in Typescript like this:
type X = 2 - 1

There are hacks https://softwaremill.com/implementing-advanced-type-level-arithmetic-in-typescript-part-1/ that potentially may screw Typescript Server performance, so in this PR I went with even better approach to generate types for depth specifically:

depth: {
allowed: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
/**
* @minItems 11
* @maxItems 11
*/
decremented: [null, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
default: 2;

depth.default we use when depth isn't passed, allowed - an enum of allowed depth values. For example we can't pass 11:
image
And depth.decremented is used for internal typescript purposes which allows us to easily decrement depth for typescript:

export type AllowedDepth = TypedDepthConfig['allowed']
export type DefaultDepth = TypedDepthConfig['default']
export type DecrementedDepth = TypedDepthConfig['decremented']
export type DecrementDepth<Depth extends AllowedDepth> = DecrementedDepth[Depth]

For example DecrementDepth<2> - result 1.

@denolfe
Copy link
Member

denolfe commented Dec 6, 2024

I'd be curious if we would benefit from introducing a type assertions tool like tsd or tstyche.

r1tsuu added a commit that referenced this pull request Dec 7, 2024
As proposed here
#9782 (comment)
with additional testing of our types we can be more sure that we don't
break them between updates.

This PR already adds types testing for most Local API methods
https://github.com/payloadcms/payload/blob/6beb921c2e232ab4edfa38c480af40a1bec1106e/test/types/types.spec.ts
but new tests for types can be easily added, either to that same file or
you can create `types.spec.ts` in any other test folder.

The new test folder uses `strict: true` to ensure our types do not break
with it.

---------

Co-authored-by: Tom Mrazauskas <[email protected]>
@r1tsuu r1tsuu force-pushed the feat/depth-generic branch from a245bf7 to 1af8521 Compare December 7, 2024 20:56
@r1tsuu
Copy link
Member Author

r1tsuu commented Dec 7, 2024

I'd be curious if we would benefit from introducing a type assertions tool like tsd or tstyche.

We have now type assertions here - https://github.com/payloadcms/payload/pull/9782/files#diff-8dec54cdc3beba0979d4e6eceb4130340bd2ee3bc91f1e779145baaeae8ea440 works perfectly!
Here are also assertions that it doesn't break current types https://github.com/payloadcms/payload/pull/9782/files#diff-5e3d49183adb6900b95513d3511004e26e5426bfe5bd555a1609d43cf1321dae (if the feature isn't enabled)

@r1tsuu r1tsuu force-pushed the feat/depth-generic branch from 8ec299c to e9979f9 Compare December 7, 2024 22:40
@r1tsuu r1tsuu force-pushed the feat/depth-generic branch from 5725d71 to ad5e9d5 Compare December 7, 2024 23:19
Copy link
Contributor

@DanRibbens DanRibbens left a comment

Choose a reason for hiding this comment

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

I'll need more time to test this on a canary version with an actual project.

| **`autoGenerate`** | By default, Payload will auto-generate TypeScript interfaces for all collections and globals that your config defines. Opt out by setting `typescript.autoGenerate: false`. [More details](../typescript/overview). |
| **`declare`** | By default, Payload adds a `declare` block to your generated types, which makes sure that Payload uses your generated types for all Local API methods. Opt out by setting `typescript.declare: false`. |
| **`outputFile`** | Control the output path and filename of Payload's auto-generated types by defining the `typescript.outputFile` property to a full, absolute path. |
| **`typeSafeDepth`** | Enable better result types for relationships depending on `depth`, disabled by default. [More Details](../queries/depth#type-safe-relationship-types-with-depth). |
Copy link
Contributor

Choose a reason for hiding this comment

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

We should enable this as the default option for all templates/examples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants