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

Add ForwardRefComponent helper type #19923

Merged
merged 3 commits into from
Oct 4, 2021

Conversation

ecraig12345
Copy link
Member

Pull request checklist

  • Include a change request file using $ yarn change

Description of changes

This PR has an alternate approach of improving typings for forwardRef components (compared to #19812): define a ForwardRefComponent type and explicitly declare it as the type of each component.

This works well with the majority of components which don't allow overriding as (see changes to Input). For Link and Text which can be rendered as multiple element types, there's a slight mismatch with the type returned by forwardRef currently requiring a cast. This isn't ideal and I'd definitely appreciate ideas for better handling, but it doesn't seem to make a difference in the overall type checking in practice.

@ecraig12345 ecraig12345 requested review from khmakoto and a team as code owners September 22, 2021 23:39
@khmakoto
Copy link
Member

Can you maybe paste here what are the mismatches found when applying it to Link and/or Text?

@ecraig12345
Copy link
Member Author

For Link the first conflict it shows is with type (a has type?: string and button has type?: 'button' | 'submit' | 'reset'). However that doesn't actually cause a problem when using the component: for a it allows any string for type, and for as="button" it limits to that list.

Type 'ForwardRefExoticComponent<Pick<(Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> &...' is not assignable to type 'ForwardRefExoticComponent<(Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Link...'.
  Types of property 'defaultProps' are incompatible.
    Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & Ref...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & RefAttrib...'.
      Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & Ref...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & RefAttrib...'.
        Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & Ref...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "a" | undefined; } & Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "slot" | ... 262 more ... | "onTransitionEndCapture"> & { ...; } & { ...; }) | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & LinkCommons & RefAttrib...'.
          Types of property 'type' are incompatible.
            Type 'string | undefined' is not assignable to type '"button" | "submit" | "reset" | undefined'.
              Type 'string' is not assignable to type '"button" | "submit" | "reset" | undefined'

For Text:

Type 'ForwardRefExoticComponent<Pick<(Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...' is not assignable to type 'ForwardRefExoticComponent<(Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> ...'.
  Types of property 'defaultProps' are incompatible.
    Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttribut...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttributes<......'.
      Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttribut...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttributes<......'.
        Type 'Partial<Pick<(Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttribut...' is not assignable to type 'Partial<Pick<{ root?: ShorthandProps<({ as?: "span" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "slot" | ... 254 more ... | "css"> & { ...; } & { ...; }) | ... 7 more ... | ({ ...; } & ... 2 more ... & { ...; })>; }, never> & Pick<...> & Partial<...> & RefAttributes<...>>'.
          Types of property 'onCopy' are incompatible.
            Type '((event: ClipboardEvent<HTMLSpanElement>) => void) | ((event: ClipboardEvent<HTMLHeadingElement>) => void) | ((event: ClipboardEvent<...>) => void) | ((event: ClipboardEvent<...>) => void) | undefined' is not assignable to type '((event: ClipboardEvent<HTMLPreElement>) => void) | undefined'.
              Type '(event: ClipboardEvent<HTMLHeadingElement>) => void' is not assignable to type '(event: ClipboardEvent<HTMLPreElement>) => void'.
                Types of parameters 'event' and 'event' are incompatible.
                  Type 'ClipboardEvent<HTMLPreElement>' is not assignable to type 'ClipboardEvent<HTMLHeadingElement>

@ecraig12345
Copy link
Member Author

All the Pick<SomeProps, 'list' | 'of' | 'everything' ........> is a rewriting of Omit<SomeProps, 'ref'> 🤦‍♀️

@fabricteam
Copy link
Collaborator

fabricteam commented Sep 22, 2021

📊 Bundle size report

Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-accordion
Accordion (including children components)
55.264 kB
17.411 kB
react-avatar
Avatar
56.703 kB
15.742 kB
react-badge
Badge
23.912 kB
7.039 kB
react-badge
CounterBadge
26.902 kB
7.753 kB
react-badge
PresenseBadge
237 B
177 B
react-button
Button
23.286 kB
7.057 kB
react-button
CompoundButton
28.598 kB
7.905 kB
react-button
MenuButton
25.48 kB
7.743 kB
react-button
Button
30.879 kB
8.858 kB
react-button
ToggleButton
32.986 kB
7.681 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
165.643 kB
46.977 kB
react-components
react-components: FluentProvider & webLightTheme
34.361 kB
11.35 kB
react-divider
Divider
15.428 kB
5.595 kB
react-image
Image
9.908 kB
3.994 kB
react-input
Input
31.652 kB
11.319 kB
react-label
Label
9.108 kB
3.729 kB
react-link
Link
11.669 kB
4.607 kB
react-make-styles
makeStaticStyles (runtime)
7.59 kB
3.321 kB
react-make-styles
makeStyles + mergeClasses (runtime)
22.136 kB
8.357 kB
react-make-styles
makeStyles + mergeClasses (build time)
2.558 kB
1.204 kB
react-menu
Menu (including children components)
103.459 kB
31.457 kB
react-menu
Menu (including selectable components)
105.735 kB
31.811 kB
react-popover
Popover
101.302 kB
30.397 kB
react-portal
Portal
6.725 kB
2.237 kB
react-positioning
usePopper
23.145 kB
7.942 kB
react-provider
FluentProvider
15.764 kB
5.784 kB
react-slider
RangedSlider
35.526 kB
10.639 kB
react-slider
Slider
32.506 kB
10.191 kB
react-switch
Switch
24.36 kB
7.858 kB
react-text
Text - Default
11.731 kB
4.443 kB
react-text
Text - Wrappers
15.353 kB
4.742 kB
react-tooltip
Tooltip
45.74 kB
15.561 kB
react-utilities
SSRProvider
213 B
170 B
🤖 This report was generated against 8952fa63a35532efafbcb5fd7290875fe838caeb

@size-auditor
Copy link

size-auditor bot commented Sep 22, 2021

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 8952fa63a35532efafbcb5fd7290875fe838caeb (build)

@codesandbox-ci
Copy link

codesandbox-ci bot commented Sep 22, 2021

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 49289b5:

Sandbox Source
@fluentui/react 8 starter Configuration
@fluentui/react-components 9 starter Configuration

@fabricteam
Copy link
Collaborator

fabricteam commented Sep 23, 2021

Perf Analysis (@fluentui/react)

No significant results to display.

All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 778 786 5000
BaseButton mount 789 777 5000
Breadcrumb mount 2270 2252 1000
ButtonNext mount 385 392 5000
Checkbox mount 1287 1277 5000
CheckboxBase mount 1095 1102 5000
ChoiceGroup mount 4016 4046 5000
ComboBox mount 828 822 1000
CommandBar mount 9179 8829 1000
ContextualMenu mount 5608 5524 1000
DefaultButton mount 944 963 5000
DetailsRow mount 3154 3267 5000
DetailsRowFast mount 6199 3214 5000
DetailsRowNoStyles mount 3019 2993 5000
Dialog mount 2111 2076 1000
DocumentCardTitle mount 136 136 1000
Dropdown mount 2725 2752 5000
FluentProviderNext mount 6278 6806 5000
FluentProviderWithTheme mount 303 298 10
FluentProviderWithTheme virtual-rerender 81 79 10
FluentProviderWithTheme virtual-rerender-with-unmount 350 343 10
FocusTrapZone mount 1555 1536 5000
FocusZone mount 1548 1558 5000
IconButton mount 1487 1481 5000
Label mount 293 290 5000
Layer mount 2547 2517 5000
Link mount 396 400 5000
MakeStyles mount 1572 1569 50000
MenuButton mount 1259 1246 5000
MessageBar mount 1729 1749 5000
Nav mount 2831 2815 1000
OverflowSet mount 946 948 5000
Panel mount 2023 1977 1000
Persona mount 715 712 1000
Pivot mount 1193 1227 1000
PrimaryButton mount 1097 1078 5000
Rating mount 6582 6588 5000
SearchBox mount 1132 1136 5000
Shimmer mount 2167 2139 5000
Slider mount 1676 1658 5000
SpinButton mount 4296 4322 5000
Spinner mount 360 347 5000
SplitButton mount 2727 2711 5000
Stack mount 417 423 5000
StackWithIntrinsicChildren mount 1484 1474 5000
StackWithTextChildren mount 3966 3956 5000
SwatchColorPicker mount 9009 8907 5000
Tabs mount 1218 1208 1000
TagPicker mount 2274 2267 5000
TeachingBubble mount 11014 11101 5000
Text mount 354 357 5000
TextField mount 1200 1172 5000
ThemeProvider mount 1007 998 5000
ThemeProvider virtual-rerender 517 512 5000
ThemeProvider virtual-rerender-with-unmount 1600 1620 5000
Toggle mount 680 687 5000
buttonNative mount 98 92 5000

Perf Analysis (@fluentui/react-northstar)

⚠️ 1 potential perf regressions detected

Potential regressions comparing to master

Scenario Current PR Ticks Baseline Ticks Ratio Regression Analysis
AlertMinimalPerf.default 232 242 0.96:1 analysis
Perf tests with no regressions
Scenario Current PR Ticks Baseline Ticks Ratio
FlexMinimalPerf.default 257 239 1.08:1
AvatarMinimalPerf.default 177 166 1.07:1
PortalMinimalPerf.default 158 149 1.06:1
ListWith60ListItems.default 567 542 1.05:1
AttachmentSlotsPerf.default 939 901 1.04:1
ButtonSlotsPerf.default 484 467 1.04:1
ChatWithPopoverPerf.default 338 324 1.04:1
HeaderSlotsPerf.default 648 625 1.04:1
ReactionMinimalPerf.default 324 312 1.04:1
RefMinimalPerf.default 206 198 1.04:1
SegmentMinimalPerf.default 301 290 1.04:1
ButtonMinimalPerf.default 152 147 1.03:1
CardMinimalPerf.default 476 462 1.03:1
StatusMinimalPerf.default 582 567 1.03:1
IconMinimalPerf.default 530 517 1.03:1
TextAreaMinimalPerf.default 435 424 1.03:1
BoxMinimalPerf.default 305 298 1.02:1
ChatDuplicateMessagesPerf.default 264 260 1.02:1
CheckboxMinimalPerf.default 2365 2328 1.02:1
EmbedMinimalPerf.default 3694 3622 1.02:1
ImageMinimalPerf.default 316 311 1.02:1
MenuMinimalPerf.default 730 715 1.02:1
PopupMinimalPerf.default 509 500 1.02:1
RadioGroupMinimalPerf.default 381 373 1.02:1
TableMinimalPerf.default 352 345 1.02:1
VideoMinimalPerf.default 545 534 1.02:1
ButtonOverridesMissPerf.default 1531 1510 1.01:1
CarouselMinimalPerf.default 394 389 1.01:1
ChatMinimalPerf.default 546 540 1.01:1
DialogMinimalPerf.default 640 633 1.01:1
ItemLayoutMinimalPerf.default 1023 1013 1.01:1
LabelMinimalPerf.default 325 322 1.01:1
MenuButtonMinimalPerf.default 1403 1395 1.01:1
RosterPerf.default 1013 1005 1.01:1
SplitButtonMinimalPerf.default 3649 3614 1.01:1
TextMinimalPerf.default 300 296 1.01:1
ToolbarMinimalPerf.default 805 797 1.01:1
TreeMinimalPerf.default 687 680 1.01:1
TreeWith60ListItems.default 154 152 1.01:1
DropdownMinimalPerf.default 2726 2727 1:1
FormMinimalPerf.default 349 348 1:1
HeaderMinimalPerf.default 308 309 1:1
InputMinimalPerf.default 1122 1117 1:1
LoaderMinimalPerf.default 597 598 1:1
ProviderMergeThemesPerf.default 1483 1484 1:1
ProviderMinimalPerf.default 981 985 1:1
SliderMinimalPerf.default 1461 1464 1:1
CustomToolbarPrototype.default 3595 3613 1:1
AnimationMinimalPerf.default 351 353 0.99:1
DividerMinimalPerf.default 304 306 0.99:1
DropdownManyItemsPerf.default 570 576 0.99:1
ListMinimalPerf.default 434 437 0.99:1
SkeletonMinimalPerf.default 301 304 0.99:1
TableManyItemsPerf.default 1593 1608 0.99:1
TooltipMinimalPerf.default 877 885 0.99:1
GridMinimalPerf.default 286 291 0.98:1
ListCommonPerf.default 542 551 0.98:1
AccordionMinimalPerf.default 133 137 0.97:1
DatepickerMinimalPerf.default 4730 4860 0.97:1
ListNestedPerf.default 476 491 0.97:1
LayoutMinimalPerf.default 306 318 0.96:1
AttachmentMinimalPerf.default 134 143 0.94:1

Copy link
Member

@khmakoto khmakoto left a comment

Choose a reason for hiding this comment

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

While I don't love that we have to use that hack on the event name this is still leagues better than the type explosion we have now. Unless someone else has a better alternative this is a good first stepping stone imo.

@@ -2,9 +2,10 @@ import * as React from 'react';
import { render } from '@testing-library/react';
import { Text } from './Text';
import { isConformant } from '../../common/isConformant';
import { TextProps } from './Text.types';
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be:

Suggested change
import { TextProps } from './Text.types';
import type { TextProps } from './Text.types';

@miroslavstastny miroslavstastny requested a review from a team September 24, 2021 08:38
*/
export type ForwardRefComponent<Props> = SomeEventName extends keyof Props
? Required<Props>[SomeEventName] extends React.PointerEventHandler<infer Element>
? React.ForwardRefExoticComponent<Props & React.RefAttributes<Element>>
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe Props should be updated like this unless we can always ensure there won't be any ref passed in.

Suggested change
? React.ForwardRefExoticComponent<Props & React.RefAttributes<Element>>
? React.ForwardRefExoticComponent<React.PropsWithoutRef<Props> & React.RefAttributes<Element>>

Copy link
Member Author

Choose a reason for hiding this comment

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

I was trying to avoid PropsWithoutRef because IIRC the odd way it's defined (either React.PropsWithoutRef or the variant in this file) increased the likelihood of type explosion. If components using this type follow our current patterns, a ref shouldn't be included, though of course there's no guarantee of that.

/**
* Return type for `React.forwardRef`, including inference of the proper typing for the ref.
*/
export type ForwardRefComponent<Props> = SomeEventName extends keyof Props
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 possibly define the type like this? ForwardRefComponent<Props = {}, RefElement extends Element = Element>. (this is just an example, I am sure the RefElement type would have to be better specified to make React happy)

This would allow us to infer the RefElement type if none was passed in as we are doing currently, but also allow easier type casting for polymorphic components.

Example might look like this:

type Props = {
  as?: "input" | "textarea" // very simplified example
  value: string
}

const InputOrTextArea: ForwardRefComponent<Props, HTMLInputElement | HTMLTextAreaElement> = React.forwardRef(...)

This can then be better inferred later on by the end user.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe RefElement extends Element = unknown and then go to full inference if RefElement extends unknown? Though I'm still not 100% sure that will fix the type mismatch.


/**
* Input component
*/
export const Input = React.forwardRef<HTMLSpanElement, InputProps>((props, ref) => {
Copy link
Member

Choose a reason for hiding this comment

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

Is the HTMLSpanElement no longer needed because InputSlots.root is a span slot? Or does InputProps need to intersect HTMLSpanElement?

import type { IntrinsicShorthandProps } from '@fluentui/react-utilities';
import * as React_2 from 'react';

// @public
export const Input: React_2.ForwardRefExoticComponent<Pick<InputProps, "input" | "slot" | "style" | "title" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "id" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "translate" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "color" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "css" | "size" | "inputWrapper" | "bookendBefore" | "bookendAfter" | "insideStart" | "insideEnd" | "as" | "appearance" | "inline"> & React_2.RefAttributes<HTMLSpanElement>>;
export const Input: ForwardRefComponent<InputProps>;

// @public (undocumented)
export interface InputCommons {
Copy link
Member

Choose a reason for hiding this comment

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

The Input interfaces should be types. Maybe not part of this PR though.

const state = useLink(props, ref);

useLinkStyles(state);

return renderLink(state);
});
// Work around some small mismatches in inferred types which don't matter in practice
}) as ForwardRefComponent<LinkProps>;
Copy link
Member

Choose a reason for hiding this comment

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

In the past I've had some bad experiences with the type inference for ISelection with DetailsList where new Selection has to be case to ISelection and somehow the constructor required params didn't match. When I see a type declared and an cast at the end, it causes a bit of worry. I think more detail about which inferred types are mismatching would help here.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's in #19923 (comment). The issue here is more with the forwardRef return type, not our custom type.

Copy link
Member Author

Choose a reason for hiding this comment

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

(Side note: see #13411 for issues with Selection constructor types...multiple people tried to fix it and couldn't come up with anything that fully worked 😢)

Copy link
Member

@ling1726 ling1726 Sep 29, 2021

Choose a reason for hiding this comment

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

Will this cast be required for all components with polymorphic as in the future ?

It seems that this might be a source of technical debt down the line

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently yes, because I haven't been able to figure out a way to make it work with the way forwardRef manipulates the returned props type. You or others are welcome to try too if you want.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a valid point, I wasn't thinking about another use case besides forwardRef as it's the way we're implementing internally, but it's valid that this might not be true for users

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this casting is a common problem when dealing with union types on Props across libraries that try to follow this similar pattern

I guess there's no good solution besides accepting for now that a casting is required.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ecraig12345 Turns out that this is a problem already solved in the internals of @types/react, they updated the implementation of PropsWithoutRef which was breaking union types when forwardRef was invoked, all we need to do is upgrade our @types/react (and probably @types/react-dom) and this will be solved

Copy link
Member Author

Choose a reason for hiding this comment

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

@bsunderhus Is the fix you're looking at present in @types/react 16, or only 17?

In latest 16.x I'm seeing this:

    type PropsWithoutRef<P> =
        // Just Pick would be sufficient for this, but I'm trying to avoid unnecessary mapping over union types
        // https://github.com/Microsoft/TypeScript/issues/28339
        'ref' extends keyof P
            ? Pick<P, Exclude<keyof P, 'ref'>>
            : P;

vs. this in latest 17.x:

        // Pick would not be sufficient for this. We'd like to avoid unnecessary mapping and need a distributive conditional to support unions.
        // see: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
        // https://github.com/Microsoft/TypeScript/issues/28339
        P extends any ? ('ref' extends keyof P ? Pick<P, Exclude<keyof P, 'ref'>> : P) : P;

Copy link
Member Author

Choose a reason for hiding this comment

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

Meant to add: if it's only in 17, we unfortunately can't take that because we haven't upgraded to 17 yet. Though we could potentially try submitting a similar fix for 16 to DefinitelyTyped.

@@ -86,6 +86,11 @@ export const divProperties: Record<string, number>;
// @public
export const formProperties: Record<string, number>;

// Warning: (ae-forgotten-export) The symbol "SomeEventName" needs to be exported by the entry point index.d.ts
Copy link
Member

Choose a reason for hiding this comment

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

Is this warning expected?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I intentionally didn't export it since we really really don't want anybody ever using that type directly (it could be exported with an @internal annotation and a big warning if the missing type creates problems, but I don't believe it should in our supported typescript versions)

? React.ForwardRefExoticComponent<Props & React.RefAttributes<Element>>
: never
: never;
// A definition like this would also work, but typescript is more likely to unnecessarily expand
Copy link
Member

Choose a reason for hiding this comment

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

Commented out code

Copy link
Member Author

Choose a reason for hiding this comment

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

This was actually intentional (at least for the PR, possibly to leave it there) as an example of something else that someone might be inclined to try changing it to in the future and why it may not work as well.

@bsunderhus bsunderhus merged commit d66bbf4 into microsoft:master Oct 4, 2021
@ecraig12345 ecraig12345 deleted the forwardref branch October 4, 2021 18:47
mlp73 pushed a commit to mlp73/fluentui that referenced this pull request Jan 17, 2022
* Add ForwardRefComponent helper type

* Change files

* update hack event name
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants