react-day-picker icon indicating copy to clipboard operation
react-day-picker copied to clipboard

types: DayPickerProps to use generic constraints

Open ongyuxing opened this issue 1 year ago • 21 comments

Context

Fix issue #1583, where passing a union type to the mode prop of DayPicker results in a TypeScript warning that the type of mode is incorrect.

Analysis

TypeScript does not support merging property types from multiple interfaces in a union type.

Solution

I updated the DayPickerProps type definition to include a generic constraint. Additionally, I provided a default type for the generic parameter to avoid breaking changes.

ongyuxing avatar Mar 08 '23 16:03 ongyuxing

@ongyuxing this seems the solution we were looking for. Thanks for the help! I'll check the tests which have too much logs to be properly debugged...

gpbl avatar Mar 08 '23 18:03 gpbl

@gpbl I tried to fix the issue that onSelect parameter inference is incorrect when mode is a union type by converting onSelect to an intersection type. But it requires using a utility types library called type-fest that includes a UnionToIntersection type. Would you recommend using this library and type for my purposes?

ongyuxing avatar Mar 08 '23 20:03 ongyuxing

Would you recommend using this library and type for my purposes?

@ongyuxing I'm interested why type-fest would be necessary? The update you propose seems to work. (The failing test is unrelated to this change.)

Also could we add an example file where we see this change working as intended? Like in website/examples.

Thanks!

gpbl avatar Mar 09 '23 10:03 gpbl

@gpbl When the mode type is a union of 'single' | 'multiple', the onSelect type will also be inferred as a union of SelectSingleEventHandler | SelectMultipleEventHandler. This will result in a TypeScript error when passing in onSelect.

To fix this error, the type of onSelect needs to be converted to SelectSingleEventHandler & SelectMultipleEventHandler, allowing TypeScript to correctly merge the types of parameters and return values. For example, the first parameter will be inferred as a union of Date | Date[].

This is an example:

import type {
  DayPickerBase,
  DayPickerDefaultProps,
  DayPickerMultipleProps,
  DayPickerRangeProps,
  DayPickerSingleProps,
  DaySelectionMode,
  SelectMultipleEventHandler,
  SelectRangeEventHandler,
  SelectSingleEventHandler,
} from 'react-day-picker'
import type { UnionToIntersection } from 'type-fest'

interface DayPickerProps<T extends DaySelectionMode = DaySelectionMode>
  extends DayPickerBase {
  mode?: T
  selected?: T extends 'single'
    ? DayPickerSingleProps['selected']
    : T extends 'multiple'
    ? DayPickerMultipleProps['selected']
    : T extends 'range'
    ? DayPickerRangeProps['selected']
    : DayPickerDefaultProps['selected']
  onSelect?: SelectEventHandler<T>
  required?: T extends 'single' ? DayPickerSingleProps['required'] : never
  min?: T extends 'multiple'
    ? DayPickerMultipleProps['min']
    : T extends 'range'
    ? DayPickerRangeProps['min']
    : never
  max?: T extends 'multiple'
    ? DayPickerMultipleProps['max']
    : T extends 'range'
    ? DayPickerRangeProps['max']
    : never
}

type SelectEventHandler<T extends DaySelectionMode = DaySelectionMode> =
  T extends 'single'
    ? SelectSingleEventHandler
    : T extends 'multiple'
    ? SelectMultipleEventHandler
    : T extends 'range'
    ? SelectRangeEventHandler
    : never

declare function DayPicker<T extends DaySelectionMode = 'default'>(
  props: Omit<DayPickerProps<T>, 'onSelect'> & {
    // Directly changing onSelect in DayPickerProps is not recommended as it will break internal references.
    onSelect: UnionToIntersection<SelectEventHandler>
  }
): JSX.Element

const DatePicker = ({ index }: { index: number }) => {
  const modes = ['single', 'multiple', 'range'] as const
  const values = [
    new Date(),
    [new Date()],
    { from: new Date(), to: new Date() },
  ]

  return (
    <DayPicker
      mode={modes[index]}
      selected={values[index]}
      onSelect={(value) => {}}
    />
  )
}

ongyuxing avatar Mar 09 '23 18:03 ongyuxing

This is the running result without any modification: image This is the running result without modifying the type of onSelect: image This is the running result after the modification: image

ongyuxing avatar Mar 09 '23 18:03 ongyuxing

@ongyuxing I see it now – thanks for the clear explanation! So yes, feel free to add the type-fest dependency. This is a great change, looking forward to release it 👍

gpbl avatar Mar 10 '23 11:03 gpbl

@gpbl It is done.🎉

ongyuxing avatar Mar 10 '23 18:03 ongyuxing

Thanks @ongyuxing . I'm seeing interesting types errors... I am looking at them.

gpbl avatar Mar 12 '23 09:03 gpbl

Sorry for hi-jacking your PR, I couldn't get actions working on your fork... I added a test and removed the need of type-fest in 6571670 (#1718).

Could we start from this change? What is missing in the test to require the type-fest utility?

gpbl avatar Mar 12 '23 11:03 gpbl

@gpbl The argument for onSelect is currently being inferred as any type. You need to add "strict: true" to the "compilerOptions" in the tsconfig.json for website. Then TypeScript will report an error.

ongyuxing avatar Mar 12 '23 12:03 ongyuxing

@gpbl The argument for onSelect is currently being inferred as any type. You need to add "strict: true" to the "compilerOptions" in the tsconfig.json for website. Then TypeScript will report an error.

LetS investigate this in another PR. I think it will fail for other cases too, not related to this change. I remember we tried to set strict mode and it wasn't working well. I cannot find the PR right now.

gpbl avatar Mar 12 '23 13:03 gpbl

OK I want to give it a second try :) I'm investigating these errors in the test: https://github.com/gpbl/react-day-picker/actions/runs/4387214125/jobs/7824904268#step:6:20

gpbl avatar Mar 18 '23 12:03 gpbl

If I make the onSelect optional, without strict mode the only thing that is failing is:

export default function Example(props: DayPickerProps) {
  return <DayPicker {...props} />;
}
Screenshot 2023-03-18 at 07 59 10

I don't think we can make much progress from here. In the upcoming version we should rethink the component prop considering this case.

gpbl avatar Mar 18 '23 13:03 gpbl

@gpbl DayPickerProps doesn't match the actual DayPicker props, but changing it will break internal references. A less elegant solution is adding a standalone DayPickerProps type for internal use.

I don't think we can make much progress from here. In the upcoming version we should rethink the component prop considering this case.

You're right! If the onSelect type is modified like this:

type SelectEventHandler = <
  T extends DaySelectionMode = DaySelectionMode
>(
  values: {
    day?: T extends 'single' ? Date : never;
    days?: T extends 'multiple' ? Date[] : never;
    range?: T extends 'range' ? DateRange : never;
    selectedDay: Date;
    activeModifiers: ActiveModifiers;
  },
  e: React.MouseEvent
) => void;

It will make writing type definitions much easier.

ongyuxing avatar Mar 18 '23 19:03 ongyuxing

Just as an experiment, I added an InternalDayPickerProps type.

ongyuxing avatar Mar 18 '23 20:03 ongyuxing

Hello, i'm facing this issue too with dayPicker, do we have any ideas when can have this merged?

balusio avatar Jun 15 '23 15:06 balusio

Any updates on this please?

samuelg0rd0n avatar Jul 13 '23 09:07 samuelg0rd0n

@samuelg0rd0n we plan to implement this in the next mayor version (still WIP), as it may be a breaking change.

gpbl avatar Jul 13 '23 12:07 gpbl

Please checkout this proposal for generic types in DayPickerProps.

Any feedback?

gpbl avatar Jul 15 '23 13:07 gpbl

Any news here ?

AntoineKM avatar Mar 20 '24 15:03 AntoineKM

Any updates on this one?

XXXVII avatar Apr 16 '24 16:04 XXXVII