-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Capture the expected payload type in commands #2537
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@@ -388,11 +390,15 @@ function onPasteForRichText( | |||
}); | |||
} | |||
|
|||
function onCopyForRichText(event: ClipboardEvent, editor: LexicalEditor): void { | |||
function onCopyForRichText( | |||
event: CommandPayloadType<typeof COPY_COMMAND>, |
There was a problem hiding this comment.
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.
event: CommandPayloadType<typeof COPY_COMMAND>, | |
event: ClipboardEvent | KeyboardEvent, |
There was a problem hiding this comment.
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>;
There was a problem hiding this comment.
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?
Closes #2536 |
Thanks for looking into this @PAkerstrand! |
There was a problem hiding this 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
export type CommandPayloadType<C> = C extends LexicalCommand<infer R> | ||
? R | ||
: never; |
There was a problem hiding this comment.
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
dispatchCommand< | ||
TCommand extends LexicalCommand<unknown>, | ||
TPayload extends CommandPayloadType<TCommand>, | ||
>(type: TCommand, payload: TPayload): boolean { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
return dispatchCommand( | ||
editor, | ||
CUT_COMMAND, | ||
event as ClipboardEvent, | ||
); |
There was a problem hiding this comment.
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?
export function dispatchCommand< | ||
TCommand extends LexicalCommand<unknown>, | ||
TPayload extends CommandPayloadType<TCommand>, | ||
>(editor: LexicalEditor, type: TCommand, payload: TPayload): boolean { |
There was a problem hiding this comment.
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
@@ -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>; |
There was a problem hiding this comment.
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.
This seems to be the core issue of the failed runs, we get an |
@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
8b6e8b2
to
2443b9b
Compare
Implements inference and enforcement of the payload type for
LexicalCommand
, as discussed in point 2. in #2536.