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

Unexpected error when using generic with Pick and Exclude #28748

Open
OliverJAsh opened this Issue Nov 29, 2018 · 14 comments

Comments

Projects
None yet
@OliverJAsh
Copy link

OliverJAsh commented Nov 29, 2018

TypeScript Version: 3.2.1

Search Terms: generic pick exclude

Code

Example 1

const fn = <Params>(
    params: Pick<Params, Exclude<keyof Params, never>>,
): Params => params;

Example 2

import React, { ComponentType, FunctionComponent } from 'react';

type User = { name: string; }

type UserProp = { user: User };

export const myHoc = function<ComposedComponentProps extends UserProp>(
  ComposedComponent: ComponentType<ComposedComponentProps>,
) {
  // These two should be equivalent, but they're not?
  // Doesn't work:
  type Props = Pick<ComposedComponentProps, Exclude<keyof ComposedComponentProps, never>>
  // Works:
  // type Props = ComposedComponentProps;

  const Component: FunctionComponent<Props> = props =>
    // Unexpected error
    <ComposedComponent {...props} />;

  return Component;
};
@iiroj

This comment has been minimized.

Copy link

iiroj commented Nov 30, 2018

I am also having this issue with version 3.2.1. The following code used to work before:
https://github.com/iiroj/breakpoint-observer/blob/52baadc23179d959f047cc07a67c0954b692778a/index.tsx#L163

EDIT:

I was able to work around this issue like so:
iiroj/breakpoint-observer@4d24c19

@deftomat

This comment has been minimized.

@sledorze

This comment has been minimized.

Copy link

sledorze commented Nov 30, 2018

Same problems here too.
Lost a day finding workarounds in our big monorepo..

@Nerlin

This comment has been minimized.

Copy link

Nerlin commented Dec 3, 2018

Same problem for me.
Broke my typing:

export type Injector<P> = <T extends P>(Component: React.ComponentType<T>) => React.ComponentType<Subtract<T, P>>;
export type Subtract<T, K> = Omit<T, keyof K>;
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

const withContext: Injector<Context> = (Component) => (props) => (
  <ContextProvider>{(context) => <Component {...context} {...props} />}</ContextProvider>
);
@JKillian

This comment has been minimized.

Copy link
Contributor

JKillian commented Dec 11, 2018

@sledorze just curious, did you find any satisfactory workarounds for this? Unfortunately it's preventing us from upgrading to TS 3.2 at the moment

@sledorze

This comment has been minimized.

Copy link

sledorze commented Dec 11, 2018

@JKillian not using Pick, not relying on key selection.

going from something like

export const myHoc = function<ComposedComponentProps extends UserProp>(
  ComposedComponent: ComponentType<ComposedComponentProps>,
) ... ```

to

```ts
export const myHoc = function<ComposedComponentProps>(
  ComposedComponent: ComponentType<ComposedComponentProps & UserProp>,
)

Unfortunately, the downside is that the types you are using must be some explicit intersection in order to infer the types correctly.

That means no more using interfaces.

I hope Typescript will provide some means to tackle this (maybe that's the case with the last version, but I'm slashing code on the backend ATM - so will not be using Typescript until 2019..).

@vict-shevchenko

This comment has been minimized.

Copy link

vict-shevchenko commented Dec 13, 2018

It will be great to hear from @typescript team if this is really a bug in 3.2.2 version or it means that previously our react HOC were typed incorrectly.

I have tried adopting a @iiroj proposed workaround, and it actually fixes TS error in HOC, but cause another issue. TS requires an injected prop on the wrapped component.

Example

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Subtract<T, K> = Omit<T, keyof K>;

interface IName {
  name: string;
}

function withName<P extends IName>(Component: React.ComponentType<P>) {
  return class WithName extends React.Component<Subtract<P, IName>> {
    public render() {
      return (
        <NameContext.Consumer>
          {(name: string)  => <Component {...this.props} name={name} />}
                                            ^^^^^ Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<P, Exclude<keyof P, "name">>> & { name: string; }' is not assignable to type 'P'.ts(2322)
        </NameContext.Consumer>
      );
    }
};

when I replace

function withName<P extends {}>(Component: React.ComponentType<P & IName>) {
  return class WithName extends React.Component<P> {
    public render() {
      return (
        <NameContext.Consumer>
          {(name: string)  => <Component {...this.props} name={name} />}
        </NameContext.Consumer>
      );
    }
};

const Named = withName(WitoutName);

<Named /> // TS Error - property 'name' is missing

I don't really want to make name prop on IName interface optional. As in 3.1.6 - TS returned a correct error for a case <Named name={'Test'} /> saying that name property is forbidden here.

@iiroj

This comment has been minimized.

Copy link

iiroj commented Dec 14, 2018

@vict-shevchenko see my example here: https://github.com/iiroj/breakpoint-observer/blob/master/stories/withBreakpoint.story.tsx

Here the <DisplayBreakpoint / requires some breakpoint props, but wrapping it with withBreakpoint correctly interprets that <CurrentBreakpoint /> has them, and that you can’t pass them manually to it.

@vict-shevchenko

This comment has been minimized.

Copy link

vict-shevchenko commented Dec 14, 2018

Hi, @iiroj, I have analyzed your example and made a small reproducible scenario based on it. Truthfully results are somehow confusing. I made a conclusion that your example work correctly only because you pass showMaxWidth. (also all props on type CurrentBreakpoint are optional)

Here is a code snippet:

import * as React from "react";

interface NameInterface {
  name: string;
};

function withName<P extends object>(Component: React.ComponentType<P & NameInterface>) {
  return function (props: P) {
    return (
      <Component
        {...props}
        name={'John'}
      />
    );
  }
}

const UnNamed_1: React.SFC<NameInterface> = ({ name }) => (<p>My name is {name}</p>);
const Named_1 = withName(UnNamed_1);
<Named_1 /> // Property 'name' is missing in type '{}' but required in type 'NameInterface'.ts(2741)


interface UnNamedPropsInterface extends NameInterface {
  age: number;
}
const UnNamed_2: React.SFC<UnNamedPropsInterface> = ({ name, age }) => (<p>My name is {name} and {age}</p>);
const Named_2 = withName(UnNamed_2);
<Named_2 age={30} /> // Property 'name' is missing in type '{ age: number; }' but required in type 'UnNamedPropsInterface'.ts(2741)


type UnNamedType = NameInterface;
const UnNamed_3: React.SFC<UnNamedType> = ({ name }) => (<p>My name is {name}</p>);
const Named_3 = withName(UnNamed_3);
<Named_3 /> // Property 'name' is missing in type '{}' but required in type 'NameInterface'.ts(2741)


type UnNamedType_2 = NameInterface & { age: number };
const UnNamed_4: React.SFC<UnNamedType_2> = ({ name, age }) => (<p>My name is {name} and {age}</p>);
const Named_4 = withName(UnNamed_4);
<Named_4 age={30} /> // All OK, but why?

Can someone please advise.
Thanks.

@iiroj

This comment has been minimized.

Copy link

iiroj commented Dec 15, 2018

Ah, so I see mine’s not really workaround but just just happens to work because of the optional props (window, and thus width doesn’t exist during SSR).

@OliverJAsh

This comment has been minimized.

Copy link

OliverJAsh commented Dec 15, 2018

Updated my original post with a much simpler example:

const fn = <Params>(
    params: Pick<Params, Exclude<keyof Params, never>>,
): Params => params;
@aprilandjan

This comment has been minimized.

Copy link

aprilandjan commented Dec 24, 2018

Also spent nearly one whole day and found this issue... How dumb I am 😭

@ramondeklein

This comment has been minimized.

Copy link

ramondeklein commented Dec 29, 2018

@aprilandjan You're not alone :-) I was following some examples and I just didn't get it to work. I thought I was doing something wrong, but it broke with TypeScript v3.2.

I have found a workaround to "fix" this issue. It's a fairly simple fix and you can apply it by spreading the properties as any instead of the typed component. You keep all type-checking on the outside, so it's a fairly non-intrusive fix. Just change:

return (<WrappedComponent {...this.props} locale={locale} />);

into

return (<WrappedComponent {...(this.props as any)} locale={locale} />);

and the error is gone.

@dupski

This comment has been minimized.

Copy link

dupski commented Dec 29, 2018

Hi Guys. Heres my complete (simplified) example which worked with TS 3.1 but does not work with TS 3.2

import * as React from "react"

const AuthContext = React.createContext({})

export type Omit<T, K extends string> = Pick<T, Exclude<keyof T, K>>

export interface IAuthContext {
    currentUser: string
}

export interface IAuthContextProp {
    auth: IAuthContext
}

export function withAuthContext<
    TComponentProps extends IAuthContextProp,
    TWrapperProps = Omit<TComponentProps, keyof IAuthContextProp>
>(
    Component: React.ComponentType<TComponentProps>
): React.ComponentType<TWrapperProps> {
    return (props: TWrapperProps): React.ReactElement<TComponentProps> => (
        <AuthContext.Consumer>{(auth) => (
            <Component auth={auth} {...props} />
        )}</AuthContext.Consumer>
    )
}

The error in TS 3.2 (on the <Component auth= ... line) is:

Type '{ auth: {}; } & TWrapperProps' is not assignable to type 'IntrinsicAttributes & TComponentProps & { children?: ReactNode; }'.
  Type '{ auth: {}; } & TWrapperProps' is not assignable to type 'TComponentProps'. ts(2322)

Thanks for your workaround @ramondeklein , it works nicely.
In my case I simply changed return (props: TWrapperProps) to return (props: any)

I wonder if this could be a problem with the React typings? For reference I am on "@types/react": "16.7.6"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment