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

Pick<Type, GenericParameter> throws new errors for attempting to access properties #28719

Closed
MLoughry opened this issue Nov 28, 2018 · 9 comments
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter Question An issue which isn't directly actionable in code

Comments

@MLoughry
Copy link

TypeScript Version: 3.2.0-rc, 9319ea4

Search Terms:
Pick, keyof

Code

interface Foo {
    a: number;
    b: boolean;
}

const SomeFunc = <K extends keyof Foo>(
    state: Pick<Foo, K>
) => {
    if (state.a) {
        state.b = true;
    }
};

Expected behavior:
No error (passes in 3.1.6)

Actual behavior:

src/example.ts:17:15 - error TS2339: Property 'a' does not exist on type 'Pick<Foo, K>'.

17     if (state.a) {
                 ~

src/example.ts:19:15 - error TS2339: Property 'b' does not exist on type 'Pick<Foo, K>'.

19         state.b = true;

Playground Link:

Related Issues:
Possibly related to #28647, but I'm unsure.

@jack-williams
Copy link
Collaborator

This implementation seems unsound given the follow instantiation.

interface NotFoo {
    a: number,
    b: string;
}

const notFoo: NotFoo = { a: 42, b: "not-boolean" };
SomeFunc<'a'>(notFoo);

@MLoughry
Copy link
Author

That's plausible. Currently, the function in question in our code base is trying to pass the parameter on to React's setState, whose types have the following comment:

        // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
        // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
        // Also, the ` | S` allows intellisense to not be dumbisense
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

In our actual usage, we're seeing if a property exists on the parameter; if so, we add more properties before calling setState:

    private setStateBasedOnMode = <K extends keyof DateSelectorState>(
        state: Pick<DateSelectorState, K>
    ) => {
        if (state.currentSelectorMode) {
            // update the shownViewStates if the currentSelectorMode is changed
            let shownViewStates = this.getShownViewState(state.currentSelectorMode);
            state.showEndTime = shownViewStates.showEndTime;
            state.showDuration = shownViewStates.showDuration;
            state.showEndDate = shownViewStates.showEndDate;
            state.showTimeOrDuration = shownViewStates.showTimeOrDuration;
            state.showTimeZones = shownViewStates.showTimeZones;
        }

        this.setState(state);
    };

Is this something that would simply need to be fixed in @types/react? And/or is there a better method on our side?

@weswigham weswigham added Question An issue which isn't directly actionable in code Domain: JSX/TSX Relates to the JSX parser and emitter labels Nov 29, 2018
@weswigham
Copy link
Member

@ahejlsberg we saw that strange pattern when we reviewed the rwc baselines for your change, if I recall. Did we decide what the better way to write that in a typesafe way was? Off the cuff I'd say a spread'd be better, personally.

@ahejlsberg
Copy link
Member

Off the cuff I'd say a spread'd be better, personally.

Agreed. A particularly troublesome aspect of the code example above is that it mutates the state input object. That could lead to some very subtle issues if a caller were to use a shared object instead of an object literal. With a spread you're guaranteed that the combined object is freshly allocated and not aliased.

@MLoughry
Copy link
Author

Spread where, exactly? To get it to work, I still seem to have to type-cast it:

    private setStateBasedOnMode = <K extends keyof DateSelectorState>(
        state: Pick<DateSelectorState, K>
    ) => {
        if ((state as DateSelectorState).currentSelectorMode) {
            // update the shownViewStates if the currentSelectorMode is changed
            let shownViewStates = this.getShownViewState((state as DateSelectorState).currentSelectorMode);
            this.setState({
                ...state,
                showEndTime: shownViewStates.showEndTime,
                showDuration: shownViewStates.showDuration,
                showEndDate: shownViewStates.showEndDate,
                showTimeOrDuration: shownViewStates.showTimeOrDuration,
                showTimeZones: shownViewStates.showTimeZones,
            });
        } else {
            this.setState(state);
        }
    };

@weswigham
Copy link
Member

    private setStateBasedOnMode = (
        state: Partial<DateSelectorState>
    ) => {
        if (state.currentSelectorMode) {
            // update the shownViewStates if the currentSelectorMode is changed
            let shownViewStates = this.getShownViewState(state.currentSelectorMode);
            this.setState({
                ...state,
                showEndTime: shownViewStates.showEndTime,
                showDuration: shownViewStates.showDuration,
                showEndDate: shownViewStates.showEndDate,
                showTimeOrDuration: shownViewStates.showTimeOrDuration,
                showTimeZones: shownViewStates.showTimeZones,
            });
        } else {
            this.setState(state);
        }
    };

?

@MLoughry
Copy link
Author

Unfortunately, no. 😔

packages/calendar/packages/calendar-common/owa-calendar-dateselector/lib/components/DateSelector.tsx:351:27 - error TS2345: Argument of type '{ showEndTime: boolean; showDuration: boolean; showEndDate: boolean; showTimeOrDuration: boolean; showTimeZones: boolean; currentSelectorMode?: DateSelectorMode; startDate?: OwaDate; endDate?: OwaDate; wasEndTimeSetManually?: boolean; shouldFocusEndTimePickerOnMount?: boolean; }' is not assignable to parameter of type 'DateSelectorState | ((prevState: Readonly<DateSelectorState>, props: DateSelectorProps) => DateSelectorState | Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode" | ... 7 more ... | "shouldFocusEndTimePickerOnMount">) | Pick<...>'.
  Type '{ showEndTime: boolean; showDuration: boolean; showEndDate: boolean; showTimeOrDuration: boolean; showTimeZones: boolean; currentSelectorMode?: DateSelectorMode; startDate?: OwaDate; endDate?: OwaDate; wasEndTimeSetManually?: boolean; shouldFocusEndTimePickerOnMount?: boolean; }' is not assignable to type 'Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode" | "showEndDate" | "showEndTime" | "showDuration" | "showTimeOrDuration" | "startDate" | "endDate" | "wasEndTimeSetManually" | "shouldFocusEndTimePickerOnMount">'.
    Property 'currentSelectorMode' is optional in type '{ showEndTime: boolean; showDuration: boolean; showEndDate: boolean; showTimeOrDuration: boolean; showTimeZones: boolean; currentSelectorMode?: DateSelectorMode; startDate?: OwaDate; endDate?: OwaDate; wasEndTimeSetManually?: boolean; shouldFocusEndTimePickerOnMount?: boolean; }' but required in type 'Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode" | "showEndDate" | "showEndTime" | "showDuration" | "showTimeOrDuration" | "startDate" | "endDate" | "wasEndTimeSetManually" | "shouldFocusEndTimePickerOnMount">'.

351             this.setState({
                              ~
352                 ...state,
    ~~~~~~~~~~~~~~~~~~~~~~~~~
...
357                 showTimeZones: shownViewStates.showTimeZones,
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
358             });
    ~~~~~~~~~~~~~

packages/calendar/packages/calendar-common/owa-calendar-dateselector/lib/components/DateSelector.tsx:360:27 - error TS2345: Argument of type 'Partial<DateSelectorState>' is not assignable to parameter of type 'DateSelectorState | ((prevState: Readonly<DateSelectorState>, props: DateSelectorProps) => DateSelectorState | Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode"
| ... 7 more ... | "shouldFocusEndTimePickerOnMount">) | Pick<...>'.
  Type 'Partial<DateSelectorState>' is not assignable to type 'Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode" | "showEndDate" | "showEndTime" | "showDuration" | "showTimeOrDuration" | "startDate" | "endDate" | "wasEndTimeSetManually" | "shouldFocusEndTimePickerOnMount">'.
    Property 'showTimeZones' is optional in type 'Partial<DateSelectorState>' but required in type 'Pick<DateSelectorState, "showTimeZones" | "currentSelectorMode" | "showEndDate" | "showEndTime" | "showDuration" | "showTimeOrDuration" | "startDate" | "endDate" | "wasEndTimeSetManually" | "shouldFocusEndTimePickerOnMount">'.

360             this.setState(state);
                              ~~~~~

@TLadd
Copy link

TLadd commented Jan 9, 2019

Also ran into this today when upgrading typescript. Similar use-case where I'm accepting an argument that gets passed to React's setState with potentially additional data added depending on which fields are passed in.

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JSX/TSX Relates to the JSX parser and emitter Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants