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

Improve IDE integration for inferred object properties to support Go To Definition, Rename Symbol, etc. #1117

Merged
merged 11 commits into from May 6, 2022

Conversation

bentefay
Copy link
Contributor

@bentefay bentefay commented May 5, 2022

This PR fixes the issue I raised here: #1115

To summarise, the properties of types declared using z.infer on z.object({ ... }) do not support IDE language server features such as Find All References, Rename Symbol or Go To Definition.

To take a concrete example using the latest version of Zod in VS Code, the language server features Find All References, Rename Symbol and Go To Definition do not work when you select the property prop in the code below:

const Example = z.object({ prop: z.string() });

type Example = z.infer<typeof Example>;

const instance: Example = { prop: "value" };

As you no doubt know, these features are incredibly useful for working on TypeScript projects, particularly as they grow in size.

These features are broken because z.object({ ... }) uses a mapped type, addQuestionMarks, that transforms its keys using requiredKeys and optionalKeys. Unfortunately, due to a limitation in the TypeScript compiler (microsoft/TypeScript#47813), transforming keys in a mapped type prevents the compiler from linking the mapped type properties to their definition.

Fortunately, it is possible to preserve the behaviour of addQuestionMarks without mapping the keys on one side of the intersection (by taking advantage of the fact that (T | undefined) & T equals T):

export type addQuestionMarks<T extends object> = {
    [k in keyof T]?: T[k];
  } & { [k in requiredKeys<T>]: T[k] };

It appears that the built-in type helpers Omit and Pick are also special cased to preserve the linkage between mapped keys, so I was able to use those for object({}).extend(), object({}).pick() and object({}).omit():

export type extendShape<A, B> = Omit<A, keyof B> & B;

These changes preserve the existing behaviour while enabling the IDE features for z.object({ ... }) as well as when using z.object({ ... }).merge(z.object({ ... })), z.object({ ... }).partial(), z.union(), z.object().pick and z.object().omit.

I have added a number of tests that use the TypeScript language server (via the library ts-morph) to verify that Go To Definition (and by extension the other features) work correctly. I have also added additional tests around the types inferred from z.object to ensure I have not broken existing behaviour.

What do you think? Happy to make any changes. Thanks!

@bentefay bentefay force-pushed the improve-ide-integration branch 2 times, most recently from 7c01ec3 to bb257f6 Compare May 5, 2022 11:09
@colinhacks colinhacks merged commit fa4eebb into colinhacks:master May 6, 2022
MrAwesome pushed a commit to MrAwesome/zod that referenced this pull request May 20, 2022
…To Definition, Rename Symbol, etc. (colinhacks#1117)

* Increase test coverage for type inference of optional object properties

* Add test showing that Go To Definition is broken on object properties

* Fix Go To Definition for inferred object properties

* Fix Go To Definition for inferred merged object properties

* Improve languageServerFeatures test names

* Fix Go To Definition for inferred pick object properties

* Fix Go To Definition for inferred omit object properties

* Exclude languageServerFeatures tests from deno

* Fix keyof issue

* 3.14.5

* Remove tc dep

Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu>
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