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: enhance types #18

Merged
merged 21 commits into from
Mar 25, 2023
Merged

feat: enhance types #18

merged 21 commits into from
Mar 25, 2023

Conversation

johnhooks
Copy link
Contributor

@johnhooks johnhooks commented Mar 13, 2023

Goal

Enhance JSDoc types to be more explicit, enable TypeScript checking of JavaScript files, and prepare for publishing first-party types.

Closes #17

Notes

Type output directory

The types are currently output to the es folder. I thought that was more appropriate than the dist folder with the UMD builds, though I can change it to whatever you think is best.

JSDoc parameter descriptors ? or =

TypeScript interprets ? as a union with null and = as a union with undefined in all the instances I believe the correct union should be undefined, that is why I change it.

Using JSDoc over TS

Generics in JSDoc are awkward and I am not familiar with the syntax. To figure out the solution, I first converted src/index.js to a TypeScript file and worked in what I was most familiar with. I wasn't able to bring all the same types into the JSDoc version.

src/index.js Outdated Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
@johnhooks
Copy link
Contributor Author

@aduth checkout this file, I typed everything like this and then ported it back into src/index.js. I think it is better typed, though to integrate TS files into the project needs more work. I just wanted to offer this up as another solution.

https://github.com/johnhooks/hpq/blob/172fec770524b0b10ed5a2d3e0ff1fd427740633/src/index.ts

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 13, 2023

I'm still learning about hpq so excuse my ignorance, it seems like matchers might be typed something like this, but I'm not sure. I would like to get rid of the any types.

type MatcherFn = (node: Element) => string|undefined;
type MatcherObj = Record<string, MatcherFn>;
type Matcher = MatcherFn | MatcherObj;

Does a MatcherFn always return string or undefined? Or is it possible to access non-string values on Element?

@aduth
Copy link
Owner

aduth commented Mar 13, 2023

Thanks for the quick turnaround on the pull request @johnhooks !

I might not be able to take a deep dive on this until the weekend, and shamefully I have to admit that it's been a while since the last major update to this library, so I'm having to refamiliarize myself with it as well!

A couple quick comments to what you've surfaced:

  • Looking at the overloading in the TypeScript version, I think I'd be open to porting this to TypeScript if it'd make it easier for you to type it
  • To your question about the return type of MatcherFn, I think conceptually I had imagined it could return anything in the sense that someone could provide their own matcher function that returns a boolean, for example. However, given the set of default matchers, I think it'd be fair to say string or undefined would be the standard expected return type, if it helps avoid the any type.
    • I'd wonder if there's any way to still support someone who might want to define their own matcher which has an arbitrary return type? Maybe a generic and inferred ReturnType?
  • I think you're correct that I had mistakenly mixed up ? and = in assigning the "optional" type
  • To your question about output directory, I think it makes sense as you've proposed, though it's also probably time for a rethink as far as what outputs the library supports broadly speaking (i.e. standardizing on ESM)

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 14, 2023

@aduth thanks! I'm excited to help.

Looking at the overloading in the TypeScript version, I think I'd be open to porting this to TypeScript if it'd make it easier for you to type it

The library seems simple, though it is actually hard to know exactly what is being accessed when the matchers are defined. I am willing to take some time and get it right. And would be happy to help maintain a TS version.

I'd wonder if there's any way to still support someone who might want to define their own matcher which has an arbitrary return type? Maybe a generic and inferred ReturnType?

I agree, there should be a way to infer the return type. It might take a little refactoring to make it work.

To your question about output directory, I think it makes sense as you've proposed, though it's also probably time for a rethink as far as what outputs the library supports broadly speaking (i.e. standardizing on ESM)

I for one would support ESM only, that shouldn't affect the largest dependent of this package, Gutenberg. Using tsc build output for modules and types would simplify the build to a single step.

It's really cool that this little library is used deep down in the core of Gutenberg. And it's obviously doing a great job because it was able to just sit for four years without any issues!

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 18, 2023

That looks really good to me.

From what I learned the only way to "overload" the function in JSDocs is a union type, and that would look pretty messy and require const parser = function() {} syntax to type it as a union of multiple functions types.

Do you want me to submit a PR with the other overloads I created and you commit the parse function signature? And we convert this to TypeScript ? It does seem to be a better fit.

@aduth
Copy link
Owner

aduth commented Mar 18, 2023

I just discovered that the newly-released TypeScript 5.0 includes an @overload directive for JSDoc, which sounds like it might be the perfect fit here.

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#overload-support-in-jsdoc

@johnhooks
Copy link
Contributor Author

That sound awesome! I'll read up on it.

@johnhooks
Copy link
Contributor Author

That just came out, I skimmed through it two days ago and didn't catch @overload, so many great improvements!

@aduth
Copy link
Owner

aduth commented Mar 18, 2023

This seems to be working okay for me in testing locally, want me to go ahead and push it up? Or do you want to take a closer look at it?

/**
 * @template {(node: Element) => any} F
 * @typedef {(node: Element) => any} MatcherFn
 */

/**
 * @template {(node: Element) => any} F
 * @typedef {Record<string, MatcherFn<F>>} MatcherObj
 */

/**
 * @template {MatcherObj<F>} O
 * @template {MatcherFn<any>} F
 *
 * @overload
 * @param {string|Element} source Source content
 * @param {O} matchers Object of matchers
 * @return {{[K in keyof O]: ReturnType<O[K]>}} Matched values, shaped by object
 */

/**
 * @template {MatcherFn<any>} F
 *
 * @overload
 * @param {string|Element} source Source content
 * @param {F} matcher Matcher function
 * @return {ReturnType<F>} Matched value
 */

/**
 * Given a markup string or DOM element, creates an object aligning with the
 * shape of the matchers object, or the value returned by the matcher.
 *
 * @template {MatcherObj<F>} O
 * @template {MatcherFn<any>} F
 *
 * @param {string|Element} source Source content
 * @param {O|F} matchers Matcher function or object of matchers
 */
export function parse(source, matchers) {
  // ...
}

@johnhooks
Copy link
Contributor Author

I just pushed something up. Sorry I can walk it back. Can I also do the comments like that rather than all spaced out like they do on Gutenberg?

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 18, 2023

@aduth I was going to walk some of it back, but it was breaking everything else. Could you take a look at the current version, and push any updates. I'll stop working on it until I hear from you.

I'm really happy with how this turned out. Thanks so much for point out @overload it was fun to take advantage of TS 5.0 so quickly.

src/index.js Outdated Show resolved Hide resolved
src/index.js Outdated Show resolved Hide resolved
@aduth
Copy link
Owner

aduth commented Mar 18, 2023

Hey, no worries and no need to walk anything back. I had to step away for a bit anyways, so it worked out. And it looks like you've got most everything in place now. Glad that new feature worked well!

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 18, 2023

Only issue I see with the new feature is that none of the comments came with the overloads, in the output es/index.d.ts

@aduth
Copy link
Owner

aduth commented Mar 18, 2023

And yes, I'd be fine to collapse the whitespace in the JSDoc if you want to do that.

@aduth
Copy link
Owner

aduth commented Mar 18, 2023

Only issue I see with the new feature is that none of the comments came with the overloads, in the output es/index.d.ts

Hm, yeah, I see what you mean 🤔 Looks like it could be partly related to microsoft/TypeScript#27981 ? (Edit: Maybe not...) Not sure if there's a syntax variation to get those comments to "stick" into the output.

@johnhooks
Copy link
Contributor Author

@aduth I tried adding the description to all the overloads, but nothing worked. Other than that, I think it's ready for review.

src/index.ts Show resolved Hide resolved
@aduth
Copy link
Owner

aduth commented Mar 18, 2023

@aduth I tried adding the description to all the overloads, but nothing worked. Other than that, I think it's ready for review.

Yeah, I didn't have any luck either. Do you think it should be a blocker? I'm okay to ship it without the comments if you are. Seems like it might be a bug in TypeScript itself. Only other option I could conceive is to port to TypeScript.

This is checked-in to source control include manual revisions documenting overloaded functions, due to a suspected upstream bug in TypeScript.

See: microsoft/TypeScript#53350
@aduth
Copy link
Owner

aduth commented Mar 19, 2023

@johnhooks I pushed up changes in 60a48ca to check-in the .d.ts file. I also created an issue on the TypeScript repository at microsoft/TypeScript#53350 in case it helps toward a resolution. Does this look okay to you?

One other thing that comes to mind is that the matcher object form supports arbitrary nesting of objects. Would it be easy enough to change the MatcherObj type from Record<string, MatcherFn<T>> to Record<string, MatcherObj<T> | MatcherFn<T>>? This is another behavior that unfortunately didn't have good test coverage 😕 , but works all the same.

Playground example:

image

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 19, 2023

@aduth That issue filed on TypeScript looks great to me.

That type makes sense to me though the type checker isn't happy with it.

/**
 * @typedef {(node: Element) => T} MatcherFn
 * @template T
 */

/**
 * @typedef {Record<string, MatcherObj<T> | MatcherFn<T>>} MatcherObj
 * @template T
 */

/**
 * @typedef {(MatcherFn<T>|MatcherObj<T>)} Matcher
 * @template T
 */

Results in:

Type alias 'MatcherObj' circularly references itself.ts(2456)

I have always had trouble understanding the nuance of circular references, sometimes they work fine.

This works though...

/**
 * @typedef {(node: Element) => T} MatcherFn
 * @template T
 */

/**
 * @typedef {{[ x: string ]: MatcherObj<T> | MatcherFn<T>} } MatcherObj
 * @template T
 */

/**
 * @typedef {(MatcherFn<T>|MatcherObj<T>)} Matcher
 * @template T
 */

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 19, 2023

But it creates a new type error in parse.

@aduth
Copy link
Owner

aduth commented Mar 19, 2023

I think I got it working in TypeScript with a combination of your suggestion of the object index signature, plus an update on the return signature of the object overload to be conditional based on the object value.

I couldn't quite get it working in JSDoc, though I did make a bit of progress by making the @template tags type-constrained like in my earlier comment.

Maybe we're back at reconsidering TypeScript 😅

https://www.typescriptlang.org/play?#code/C4TwDgpgBAsghsAxgCwgJwGIDsA8GB8UAvFABRYD2AJhAFxQCiANhALYRbACUxhcWIANwAoUJFgIU6APIAjAFZ5CJAN5QA2gA96AZ2BoAllgDmAXXrwkqNHMUEoAHwlX02JVAC+w4QDMArliIwAYUWFBgcGg6EHhQEJrAHFQ6zlKYuPwg+AA0UNJxCUkplmm2Svjk1HSMLOycuayS1jr00lz0KuoA0lBGUADWECAUPnnmed2mBYlYyVAYUAD8UABKEMB+aFgAKuAx0pOErZMeIv6BwaHhkdGx8TNzJdZumfgVlDT0zGwcwA1N6Ba83aq3Wmx2eyUZwCQRCYQiURiC3uRVSzwyAhyeWmqKeMgU5UqnxqP3qUEaLiirUcwKgKmEUEZUDQYK2dNOwi8wkQoT0zIgOj8TGAxGuiNIVAoiD8dWAADpZNQQLk1D4KBQOlBZJF6ESIDwiIQPhA5YkEgBhUIzEUeTxcEQ8rB8lmC4UAJlFCOiEqlMt+CqVuVIiqoIANhBDIDlRiw6AAEtsYAAZe1AA

@aduth
Copy link
Owner

aduth commented Mar 19, 2023

@johnhooks Looks like we both had the same idea this afternoon, I had also gone down this path of porting in 489bcdf, but it looks like you beat me by a few minutes. 😄

@johnhooks
Copy link
Contributor Author

Check it out, the only thing that I couldn't figure out was how to run the tests.

@johnhooks
Copy link
Contributor Author

The overloads have all the comments now, like they should.

@johnhooks
Copy link
Contributor Author

johnhooks commented Mar 19, 2023

It looks like you know how to register babel for tests with ts extensions! Awesome.

@johnhooks
Copy link
Contributor Author

I'm going to pause on this. There are still some issues to resolve, but it seemed like I might have been stepping on your toes! I'll wait to hear from you. Also, it seemed like you were getting in to it, do you want to implement the same for rememo and memize, or would you like help?

@aduth
Copy link
Owner

aduth commented Mar 19, 2023

@johnhooks No toe-stepping at all! I had dug a little deeper than I expected out of curiosity, and should have communicated earlier. I'm not too worried from my point-of-view about having duplicated the work, since it was an interesting TypeScript challenge. If you want to go ahead and work through those remaining issues, please feel free. Maybe there are some useful bits from my commit that you can use (e.g. I noticed that the object return type I mentioned in my earlier comment needed a bit more work to resolve the return type of deeply nested functions). My availability may be a bit spottier again over the next days, but if you reach a good stopping point, let me know and I'll try to make some time to help publish the finished version.

Similarly, I may not have the bandwidth to take a look at those other projects in the short term, so if you want to take a stab at it, I'd appreciate that.

@johnhooks johnhooks force-pushed the feat/enhance-types branch from 3312c04 to b530fe2 Compare March 20, 2023 03:47
Copy link
Contributor Author

@johnhooks johnhooks left a comment

Choose a reason for hiding this comment

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

@aduth this PR is ready for a review. I'm very happy with the solutions we came up with. Thank you so much for your help and support.

.gitignore Outdated
@@ -1,4 +1,4 @@
es/
/es/*
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aduth I think this is from when we were going to check in the es/index.d.ts file, should it be reverted back to the original?

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, I think we could revert it to simplify the diff, but this appears to work just as well, so I'm also fine to keep as-is.

src/get-path.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Show resolved Hide resolved
Copy link
Owner

@aduth aduth left a comment

Choose a reason for hiding this comment

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

This is all looking fantastic! Thanks for bearing with me through all the iterations. I think it turned out great in the end. I'll plan to get this merged and published either later today or at least by the end of the weekend.

.gitignore Outdated
@@ -1,4 +1,4 @@
es/
/es/*
Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, I think we could revert it to simplify the diff, but this appears to work just as well, so I'm also fine to keep as-is.

src/get-path.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Show resolved Hide resolved
Copy link
Contributor Author

@johnhooks johnhooks left a comment

Choose a reason for hiding this comment

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

@aduth, it is ready for final review.

Copy link
Owner

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Awesome! Thanks again for all your work with this. Excited to see it help with your work typing Gutenberg packages. I'll be merging and publishing shortly.

@aduth aduth merged commit 0e48077 into aduth:master Mar 25, 2023
@aduth
Copy link
Owner

aduth commented Mar 25, 2023

This is now published as [email protected] 🚀

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.

Improving the JSDoc types and generating a declaration file
2 participants