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

Capture the expected payload type in commands #2537

Conversation

PAkerstrand
Copy link
Contributor

@PAkerstrand PAkerstrand commented Jun 27, 2022

Implements inference and enforcement of the payload type for LexicalCommand, as discussed in point 2. in #2536.

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 27, 2022
@vercel
Copy link

vercel bot commented Jun 27, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
lexical ✅ Ready (Inspect) Visit Preview Jun 30, 2022 at 10:41AM (UTC)
lexical-playground ✅ Ready (Inspect) Visit Preview Jun 30, 2022 at 10:41AM (UTC)

@@ -388,11 +390,15 @@ function onPasteForRichText(
});
}

function onCopyForRichText(event: ClipboardEvent, editor: LexicalEditor): void {
function onCopyForRichText(
event: CommandPayloadType<typeof COPY_COMMAND>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we just use a simple union type here instead? It might be a little more readable and CommandPayloadType feels slightly too general if we're not using it a lot elsewhere.

Suggested change
event: CommandPayloadType<typeof COPY_COMMAND>,
event: ClipboardEvent | KeyboardEvent,

Copy link
Contributor

@acywatson acywatson Jun 29, 2022

Choose a reason for hiding this comment

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

Since these are actually command listeners, as opposed to DOM Event Listeners, I think the abstraction here is appropriate.

I was going to actually suggest going a step further and having

type CopyCommandPayload = typeof COPY_COMMAND;
...
event: CommandPayloadType<CopyCommandPayload>;

@zurfyx @fantactuka @tylerjbainbridge ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@thegreatercurve I actually started with just a union type. The question then becomes, do we want to provide a utility type for extracting the payload in a nice way from the lexical package, to make it a bit easier for other users if they want to extract a function for delegating to from the command handlers? Inside of the command handlers the payload is inferred, but that's only the case if/when it is inlined. If you want to do something like this:

type MY_COMMAND = createCommand<string | SomeOtherType>();

// ...
editor.registerCommand(MY_COMMAND, myCommandHandler);

// ...
function myCommandHandler(payload: string | SomeOtherType) {}

The idea here was to provide functionality for this, such that the payload type and the argument type are can easily be kept in sync if that union type would grow for some reason

function myCommandHandler(payload: CommandPayloadType<typeof MY_COMMAND>) {}

If we do want to provide it, it would make a lot of sense to adorn it with a proper TSDoc comment to show its purpose and some examples of how to use it.

I guess it's also a question of:

  • How often do the generic type of a command really change?
  • Where would we want type errors in usage to pop up?

@thegreatercurve
Copy link
Contributor

Closes #2536

@thegreatercurve
Copy link
Contributor

Thanks for looking into this @PAkerstrand!

Copy link
Member

@zurfyx zurfyx left a comment

Choose a reason for hiding this comment

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

Thanks for taking a stab at this, we certainly missed this when we migrated to TS and it's pretty important to get this right! Turns out TS resolves the type whereas Flow takes the first type available so we can no longer leverage the empty object and unused generic trick

Wrote down a couple comments, the idea is correct but I think the clipboard types needs fixing..
I wrote a simple version outlining the gist of the problem, it comes down to a current issue on the existing codebase I think (lmk if I'm mistaken, I checked pretty quick) 5f743c9

packages/lexical/src/LexicalEditor.ts Outdated Show resolved Hide resolved
packages/lexical-plain-text/src/index.ts Show resolved Hide resolved
Comment on lines 171 to 173
export type CommandPayloadType<C> = C extends LexicalCommand<infer R>
? R
: never;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved this type helper here since it is quite tightly tied to the LexicalCommand-type

packages/lexical/src/LexicalEditor.ts Outdated Show resolved Hide resolved
Comment on lines +637 to +663
dispatchCommand<
TCommand extends LexicalCommand<unknown>,
TPayload extends CommandPayloadType<TCommand>,
>(type: TCommand, payload: TPayload): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@thegreatercurve Now we have some internal usage of the CommandPayloadType, namely to ensure that the type of the payload matches what's extracted from the type of the first argument

@@ -482,7 +482,7 @@ function onBeforeInput(event: InputEvent, editor: LexicalEditor): void {
if (inputType === 'insertText') {
if (data === '\n') {
event.preventDefault();
dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, undefined);
dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

New type errors that popped up from the stricter enforcement.

A LexicalCommand<boolean> no longer accepts undefined or null as payload

Comment on lines +970 to +979
return dispatchCommand(
editor,
CUT_COMMAND,
event as ClipboardEvent,
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are we fine with these type casts, or do we need to refactor the code such that TS can properly infer the type itself?

Comment on lines +1102 to +1105
export function dispatchCommand<
TCommand extends LexicalCommand<unknown>,
TPayload extends CommandPayloadType<TCommand>,
>(editor: LexicalEditor, type: TCommand, payload: TPayload): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same signature as in LexicalEditor.ts

@PAkerstrand
Copy link
Contributor Author

I keep getting these types of failures in the test suite, and it's usually on Windows and composition. Not sure whether it's coincidence or if I've accidentally introduced some errors? Not sure how I could introduce connection refused errors though.

image

@@ -167,7 +167,30 @@ export const COMMAND_PRIORITY_HIGH = 3;
export const COMMAND_PRIORITY_CRITICAL = 4;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type LexicalCommand<T = never> = Readonly<Record<string, unknown>>;
export type LexicalCommand<TPayload> = Record<string, never>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out it was the Readonly type helper that breaks type inference later on. Record<string, never> signifies an empty object, which is what the commands currently are, but still allows the generic type to be extracted later on.

@PAkerstrand
Copy link
Contributor Author

I keep getting these types of failures in the test suite, and it's usually on Windows and composition. Not sure whether it's coincidence or if I've accidentally introduced some errors? Not sure how I could introduce connection refused errors though.

image


This seems to be the core issue of the failed runs, we get an EMFILE-error emitted. Might be some file descriptor leakage somewhere?

image

@acywatson
Copy link
Contributor

acywatson commented Jul 4, 2022

@thegreatercurve - the author let me know he is on vacation for the next 3 weeks - can you take this over and see if we need to make any changes or if we can merge it as-is?

The CI failure is the Windows thing I fixed and should pass on rebase (or we can ignore that signal and merge as-is if you're good)

@PAkerstrand
Copy link
Contributor Author

@thegreatercurve - the author let me know he is on vacation for the next 3 weeks - can you take this over and see if we need to make any changes or if we can merge it as-is?

The CI failure is the Windows thing I fixed and should pass on rebase (or we can ignore that signal and merge as-is if you're good)

Correct ☝🏼

I’m completely OK with you pushing any changes required to get this merged. I am somewhat around to be able to respond, but won’t be coding for the next few weeks

Previously typed as `string`, updated to `InputEvent | string`
The `jsdom`-environment in Jest does not have `ClipboardEvent` globally
defined, causing the unit tests to fail
Additionally, use generic contraints to enforce the command type and
payload type to match.

Fix type errors from stricter enforcements introduced
add some TSDoc for `CommandPayloadType`-helper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants