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

Proposal setup to make the TypeScript migration easier #614

Closed
wants to merge 16 commits into from

Conversation

ghostd
Copy link

@ghostd ghostd commented Jun 9, 2020

What:

Add a TypeScript config to generate the declaration files from the TS source files.

Why:

The aim is to make the TS transition easier (and avoid big PR as mentioned here)

How:
I added a "manual" script generate-types which generates the declaration files from the TS source files. This approach allows us to migrate each JS file one by one. I don't know what could be the best way to insert this command into the build/validation process.

This PR includes a first migration (see get-node-text.ts)

Checklist:

  • Documentation added to the
    docs site
  • I've prepared a PR for types targeting
    DefinitelyTyped "N/A"
  • Tests
  • Ready to be merged

Any thoughts are welcome.

#494

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 9, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 3e8c451:

Sandbox Source
kentcdodds/react-testing-library-examples Configuration

@ghostd
Copy link
Author

ghostd commented Jun 9, 2020

I'm not sure to understand why the build fails. The "typecheck" step works for me :-/

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

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

Maybe the reason it's working on your machine and not on CI is a version mis-match. I think fixing the error should be pretty straightforward though.

This is looking pretty good. Could you try and move one other file to TypeScript so we get a more realistic view of what this would be like?

babel.config.js Outdated Show resolved Hide resolved
Disable some tslint rules which conflicts with the declarations generated by tsc
Also remove the useless babel.config.js
Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

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

Feeling pretty comfortable with this generally. I'm open to seeing what this looks like if the repo is 100% TypeScript.

@@ -0,0 +1,72 @@
declare type EventMapKey = string
Copy link
Member

Choose a reason for hiding this comment

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

Are .d.ts files necessary when the source is written in TS?

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't be

Copy link
Author

Choose a reason for hiding this comment

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

When the migration will be over, the types directory can be removed from the source; the declaration files will be generated during the build phase.

Copy link
Member

Choose a reason for hiding this comment

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

Can we not start generating the types before everything is migrated?

Copy link
Author

Choose a reason for hiding this comment

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

Not sure. tsc uses the typescript source files to extract the declarations. i have no idea how to do that before everything is migrated. If someone knows a way i'd be very glad to try it.

Choose a reason for hiding this comment

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

I'm not 100% sure I understand what you're wanting to do but tsc only runs on typescript files so if you want it to generate typescript definition files for only those that have been moved over that should happen naturally. I'm sure we could find what you're looking for in the compiler docs. But if you clarify what you are looking to do I could do more research.

tests/jest.config.dom.js Outdated Show resolved Hide resolved
tests/jest.config.node.js Outdated Show resolved Hide resolved
@@ -1 +1,2 @@
export function getNodeText(node: HTMLElement): string;
declare function getNodeText(node: HTMLElement): string;
export { getNodeText };
Copy link
Member

Choose a reason for hiding this comment

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

Same here. Do we need .d.ts files when the source is in TypeScript?

Choose a reason for hiding this comment

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

shouldn't need to. Might need to add the -d flag when compiling or set that in your tsconfig.

It seems the prettier settings can conflit with the 'whitespace' rule of dtslint.

Also forces the dtslint version to be sure Travis use a version with this fix: microsoft/dtslint#295
types/event-map.d.ts Outdated Show resolved Hide resolved
@codecov
Copy link

codecov bot commented Jun 10, 2020

Codecov Report

Merging #614 into master will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##            master      #614   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           24        24           
  Lines          555       559    +4     
  Branches       138       141    +3     
=========================================
+ Hits           555       559    +4     
Impacted Files Coverage Δ
src/event-map.ts 100.00% <100.00%> (ø)
src/get-node-text.ts 100.00% <100.00%> (ø)
src/matches.ts 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 281eb8b...3e8c451. Read the comment docs.

kentcdodds
kentcdodds previously approved these changes Jun 10, 2020
Copy link
Member

@kentcdodds kentcdodds 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 great. I'm happy to merge this as-is and then we can work on converting the rest over. But I'd like another 👍 from someone before I do.

@@ -1,95 +1,104 @@
export type EventType =
Copy link
Member

@smeijer smeijer Jun 11, 2020

Choose a reason for hiding this comment

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

Can't we use the native HTMLElementEventMap or WindowEventMap instead of maintaining a custom type?

Copy link
Member

Choose a reason for hiding this comment

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

Didn't know this existed. I'd prefer this 👍

Copy link
Member

@smeijer smeijer Jun 11, 2020

Choose a reason for hiding this comment

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

Yeah, addEventListener is typed, so my first thought is that we should be able to reuse those types.

// HTMLDocument
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

// HTMLElement
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

@ghostd, please correct me if I'm wrong. I might miss something fundamental.

Copy link
Author

Choose a reason for hiding this comment

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

From my understanding, removing the custom EventType will not prevent us to maintain the event map (since the metadata are 'erased' at runtime), and for now, the lib.dom.ts is still missing some events (composition ones and maybe others).
Maybe we can have a custom type which would be a union of the DocumentEventMap, HTMLElementEventMap and the missing events used by dom-testing-library. In this way we will just maintain the missing types. I'll give a shot

Copy link
Author

Choose a reason for hiding this comment

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

I think i misunderstood your point.

We can simply define EventType this way:

type EventType = keyof (typeof eventMap);

When we add an entry into eventMap, TS will infer the new definition of EventType

Does this answer to your point?

Copy link
Member

Choose a reason for hiding this comment

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

I think I would prefer this solution.

@eps1lon, what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good to me. You two seem to know more about built-in types for this problem so I can't help much.

Copy link
Author

Choose a reason for hiding this comment

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

I found this as an example from the documentation:

fireEvent.keyDown(domNode, { key: 'Enter', code: 'Enter' })

The case of the keys of eventMap seems to be part of the public API. Changing this would be a breaking change. I propose to have keydown in eventMap (so the key will be type checked against the lib.dom.ts interface), and add keyDown into eventAliasMap

I'll do a commit this day to show the changes.

Copy link
Author

Choose a reason for hiding this comment

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

I've started something, and now i've to maintain the camelcase event list into the eventAliasMap:

export const eventAliasMap: Record<string, EventType> = {
  doubleClick: 'dblclick',

  // Aliases to keep the API with camelcase events

  // Composition Events
  compositionEnd: 'compositionend',
  compositionStart: 'compositionstart',
  compositionUpdate: 'compositionupdate',
  // Keyboard Events
  keyDown: 'keydown',
  keyPress: 'keypress',
  keyUp: 'keyup',
  // Focus Events
  focusIn: 'focusin',
  focusOut: 'focusout',

  // Mouse Events
  contextMenu: 'contextmenu',
  dblClick: 'dblclick',
  dragEnd: 'dragend',
  dragEnter: 'dragenter',
  dragExit: 'dragexit',
  dragLeave: 'dragleave',
  dragOver: 'dragover',
  dragStart: 'dragstart',
  mouseDown: 'mousedown',
  mouseEnter: 'mouseenter',
  mouseLeave: 'mouseleave',
  mouseMove: 'mousemove',
  mouseOut: 'mouseout',
  mouseOver: 'mouseover',
// ...more and more...
}

We will to need an AliasEventType to correctly declare the all the fireEvent aliases (fireEvent.dblClick, fireEvent.mouseEnter, and so on). It seems we just shift the problem a little further.

The library API exposes camelcase events, which is more usable/readable for the end user IMHO. Maybe we should wait for the end of the TS migration before trying to simplify the code. For now, TS should be a "helper", not a constraint to build a clean API.

If you really want to see all the modifications i can spend more time on it this weekend. Let me know your opinions

Copy link
Member

@smeijer smeijer Jun 12, 2020

Choose a reason for hiding this comment

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

If you really want to see all the modifications i can spend more time on it this weekend

Nah, let's not waste your time. I agree with:

Maybe we should wait for the end of the TS migration before trying to simplify the code.

We've (you've) explored the available options. It looks like we have the best one for this moment.

So let's get this thing merged 👍

} & KeyboardEventInit
declare type EventMapValue =
| {
EventType: 'AnimationEvent'
Copy link
Member

@smeijer smeijer Jun 11, 2020

Choose a reason for hiding this comment

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

Do we want EventType here to be a string, or the type it represents from lib.dom?

EventType: 'AnimationEvent' | AnimationEvent

Not sure which I favor. I'm just curious about the reason why (pro/cons).

Copy link
Author

Choose a reason for hiding this comment

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

The string is used at runtime:

const EventConstructor = window[EventType] || window.Event
let event
/* istanbul ignore else */
if (typeof EventConstructor === 'function') {
event = new EventConstructor(eventName, eventInit)
} else {
// IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
event = window.document.createEvent(EventType)

So we need a TS macro to transform a TS interface name into string. I don't know how to do that.

@kentcdodds
Copy link
Member

I think we'll want to put a hold on this until #616 is merged (which should be early next week).

@nickserv
Copy link
Member

nickserv commented Jun 13, 2020

Types can often help with big refactors/additions like this, there's often some extra churn involved but I usually feel it's worth it. Do you have any concerns about if merging this first would cause issues, or is it just that you'd prefer to prioritize the merge first?

@kentcdodds
Copy link
Member

It's just because I think we may not be quite ready to merge this. And I really want to prioritize merging in user event.

@nickserv
Copy link
Member

Thanks I'm fine with that, I just wasn't sure about the status

@nickserv
Copy link
Member

nickserv commented Jun 16, 2020

FYI: The user-event merge has been cancelled. We should be ready to work on this again if @kentcdodds is okay with it. Let me know if you need help with anything.

@ghostd
Copy link
Author

ghostd commented Jul 24, 2020

Here are the rest of the migration (only "src", not "tests") before my vacation.

I think we can remove dtslint now the source uses TypeScript (and add the .d.ts files into .gitignore).

@kentcdodds
Copy link
Member

Thanks @ghostd.

Obviously there are a handful of merge conflicts which will need to be resolved before this is merged, but I'd like it if @testing-library/core-maintainers could review this, provide feedback, and we need to decide whether this is something we want to pursue.

@marcosvega91
Copy link
Member

Thanks @ghostd.

Obviously there are a handful of merge conflicts which will need to be resolved before this is merged, but I'd like it if @testing-library/core-maintainers could review this, provide feedback, and we need to decide whether this is something we want to pursue.

I'm a great fun of Typescript but sometimes it requires more time to understand and use, especially when types used are very restrictive. It could help to avoid some errors but at the same time we have a lot of tests to avoid them.
Typescript is also known by less people than JavaScript so it could be more difficult for newcomers to contribute to this library.

@kentcdodds
Copy link
Member

it could be more difficult for newcomers to contribute to this library.

That's precisely why I've not added TypeScript to any of my open source projects.

@@ -0,0 +1,55 @@
import * as defaultQueries from './queries'

Copy link
Collaborator

Choose a reason for hiding this comment

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

This area seems the most complex in the migraiton... I know we've gone through a few iterations on the typings for this. Hopefully it doesn't need to change often.

function fuzzyMatches(textToMatch, node, matcher, normalizer) {
export type MatcherFunction = (content: string, element: HTMLElement) => boolean

export type Matcher = string | RegExp | MatcherFunction
Copy link
Collaborator

Choose a reason for hiding this comment

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

The types for these options are quite helpful

{exact = true, collapseWhitespace, trim, normalizer} = {},
container: HTMLElement,
value: Matcher,
{exact = true, collapseWhitespace, trim, normalizer}: MatcherOptions = {},

This comment was marked as off-topic.

@@ -35,13 +38,19 @@ function queryAllLabelsByText(
})

return matcher(textToMatch, label, text, matchNormalizer)
})
}) as HTMLElement[]
Copy link
Collaborator

@alexkrolick alexkrolick Jul 27, 2020

Choose a reason for hiding this comment

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

This isn't inferrable? I wonder what the type is without the as, when you filter a NodeList.

Copy link
Member

Choose a reason for hiding this comment

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

This is widening the type from HTMLLabelElement[]. Though I don't know why this is intended.

Copy link
Collaborator

@alexkrolick alexkrolick Jul 27, 2020

Choose a reason for hiding this comment

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

Actually a nice constraint would be to add a queryAll function type that has HTMLElement[] as the return type. Then if we mess up the implementation to return something else, it would break. Something like this:

function queryAllLabelsByText(/* */): QueryAllResult {

}

I'm not sure about the widening part.

Copy link
Member

Choose a reason for hiding this comment

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

Returning an HTMLLabelElement if HTMLElement is expected is perfectly fine though since HTMLLabelElement extends HTMLElement. The other way around would be problematic.

@MichaelDeBoey
Copy link
Member

kcd-scripts is now includes it's own typecheck script & auto-generates type files 🥳🎉

https://github.com/kentcdodds/kcd-scripts/releases/tag/v7.2.0

@marcosvega91
Copy link
Member

Maeby we can start migrating @testing-library/react that is smaller and than we can try with this ?

@nickserv
Copy link
Member

nickserv commented Nov 28, 2020

I think it would still be best to start here, as most packages (including React Testing Library and User Event) depend on it. If we migrate dependent packages first, and then change the types emitted in DOM Testing Library, then we may have to update the dependent packages again.

Finishing migrating DOM Testing Library (including using this script and resolving merge conflicts) first may be more work up front, but I think it will be less work in the long run and it will make it easier to keep types correct when migrating dependent packages. Let me know if any help is needed.

@kentcdodds
Copy link
Member

I think it would be best to avoid doing the migration all at once and instead do it over time and only start generating the type defs when we're finished.

@nickserv
Copy link
Member

nickserv commented Nov 29, 2020

Does that mean that we're updating or closing this pull request? I may be able to help but I'm not sure where to continue.

@kentcdodds
Copy link
Member

Probably much easier to close at this point

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.

9 participants