Skip to content

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 <tom@mrazauskas.de>
@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.

Copy link

Choose a reason for hiding this comment

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

@DanRibbens and @r1tsuu Would it be possible to mark this feature as experimental, and include it in a release? That way people can start using it if they opt into experimental usage, and collect feedback from real-world usage.

Choose a reason for hiding this comment

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

I'd be happy to try this out on our project if it ended up behind an experimental flag.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it'd be reasonable, but we shouldn't enable it for templates/examples then at the start, but rather after some feedback on this feature.

Copy link

Choose a reason for hiding this comment

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

@DanRibbens What are your thoughts on this?

@V1RE
Copy link

V1RE commented Feb 13, 2025

@r1tsuu @DanRibbens Sorry for bothering you guys once again, but this feature is absolutely mindblowing. Is there anything I can do to help this feature get shipped in the next update?

@DanRibbens
Copy link
Contributor

@r1tsuu @DanRibbens Sorry for bothering you guys once again, but this feature is absolutely mindblowing. Is there anything I can do to help this feature get shipped in the next update?

You're not bothering us at all. Thank you for the nudge. I will try and make more time for this soon.

kendelljoseph pushed a commit that referenced this pull request Feb 21, 2025
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 <tom@mrazauskas.de>
@robertmalicke
Copy link

Fantastic feature, excited to see it, great job Payload team

@r1tsuu r1tsuu force-pushed the feat/depth-generic branch from 71c41ba to cd9262e Compare March 5, 2025 02:14
@V1RE
Copy link

V1RE commented Mar 11, 2025

@r1tsuu Is it possible to somehow start using this inside a project? If not, pkg.pr.new might be interesting so the community can start testing these types of changes

@V1RE
Copy link

V1RE commented Apr 1, 2025

@DanRibbens By any chance had time to look into this?

@crcorbett
Copy link

This feature would be massively helpful! 🙏

@renekahr
Copy link

Any updates on this?

@ericwaetke
Copy link

I’m super stoked for this as well and can’t wait for this feature to be ready. This would dramatically improve my code quality!

@renekahr
Copy link

@ericwaetke Agree!
By the way, how do you currently solve this?
I can't find a proper way to type my queries that use depth without either writing the types myself or checking for the existence of each field in the code.

@ericwaetke
Copy link

ericwaetke commented Aug 6, 2025

@ericwaetke Agree! By the way, how do you currently solve this? I can't find a proper way to type my queries that use depth without either writing the types myself or checking for the existence of each field in the code.

I’ve got a function like this, pretty sure it’ll work most of the time

async function fetchOrReturnRealValue<T extends keyof Config['collections']>(
	item: number | Config['collections'][T],
	collection: T
): Promise<Config['collections'][T]> {
	if (typeof item === "number") {
		const payload = await getPayload()
		return await payload.findByID({
			collection,
			id: item
		}) as Config['collections'][T]
	} else {
		return item as Config['collections'][T]
	}
}
@DanRibbens
Copy link
Contributor

@r1tsuu we should revisit this one.

@Patrickroelofs
Copy link
Contributor

Wondering if this could get a follow-up, this PR increases DX by a lot!

@kolyasya
Copy link

I wish this one to be implemented. This is really annoying in terms of DX to have a string | [Collection] type

@HananoshikaYomaru
Copy link

@r1tsuu does this PR make the select and join option in the query statement type safe as well?

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