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<T, Exclude<keyof T, K>> & Pick<T, Extract<keyof T, K>> should be assignable to T #28884

Open
djrodgerspryor opened this issue Dec 6, 2018 · 17 comments
Labels
Domain: Conditional Types The issue relates to conditional types Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Milestone

Comments

@djrodgerspryor
Copy link

TypeScript Version: 3.2.1

Search Terms:
3.2.1
extends
intersection generic

Code

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

type Func<T> = (arg: T) => null

type Context = 'Context';

export function withRouteContextPropConsumer<
    T extends { routeContext: Context }
>(
    funcToWrap: Func<T>,
): Func<Omit<T, "routeContext">> {
    return (args: Omit<T, "routeContext">) => {
        const routeContext: Context = 'Context';
        return funcToWrap({ ...args, routeContext });
    };
}

Expected behavior:
Code compiles without errors

Actual behavior:

Argument of type '{ routeContext: "Context"; }' is not assignable to parameter of type 'T'.

Playground Link: http://www.typescriptlang.org/play/#src=type%20Omit%3CT%2C%20K%20extends%20keyof%20T%3E%20%3D%20Pick%3CT%2C%20Exclude%3Ckeyof%20T%2C%20K%3E%3E%3B%0A%0Atype%20Func%3CT%3E%20%3D%20(arg%3A%20T)%20%3D%3E%20null%0A%0Atype%20Context%20%3D%20'Context'%3B%0A%0Aexport%20function%20withRouteContextPropConsumer%3C%0A%20%20%20%20T%20extends%20%7B%20routeContext%3A%20Context%20%7D%0A%3E(%0A%20%20%20%20funcToWrap%3A%20Func%3CT%3E%2C%0A)%3A%20Func%3COmit%3CT%2C%20%22routeContext%22%3E%3E%20%7B%0A%20%20%20%20return%20(args%3A%20Omit%3CT%2C%20%22routeContext%22%3E)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20routeContext%3A%20Context%20%3D%20'Context'%3B%0A%20%20%20%20%20%20%20%20return%20funcToWrap(%7B%20...args%2C%20routeContext%20%7D)%3B%0A%20%20%20%20%7D%3B%0A%7D
Related Issues: Nothing obvious

After upgrading from 3.0.3 to 3.2.1, it seems that tsc has (at least partially) lost the ability to reason about constrained generics.

In the example above (one of our React context helper functions, modified to remove the React dependency), the function is parameterised over a constrained generic:

T extends { routeContext: Context }

But a few lines later, the compiler complains that the generic T may not have a routeContext attribute. T must have a routeContext attribute however, because of the constraint. Perhaps the Omit helper is confusing things?

@weswigham
Copy link
Member

Because it's not valid~

Consider if

enum AContext {
	Context = "Context",
	OtherContext = "OtherContext"
}

You can have T={ routeContext: AContext.Context }, and a "Context" is not going to be assignable to a AContext.Context.

But beyond that, I'm pretty sure we don't unify an Omit + omitted props back into the underlying type parameter today, so even fixing the above (to assign a T["routeContext"] instead of a Context), I'm not sure it'd work.

@weswigham weswigham added Experience Enhancement Noncontroversial enhancements Domain: Conditional Types The issue relates to conditional types labels Dec 6, 2018
@punmechanic
Copy link

punmechanic commented Dec 7, 2018

@weswigham FWIW, I have a similar issue in my code using higher order components in React, but have the type constraint T extends Partial<Other> and still encounter what appears to be the same problem:

function decorate<P extends Partial<OwnProps>>(c: React.ComponentType<P>) {
  type RequiredProps = Pick<P, Exclude<keyof P, keyof OwnProps>>
  // RequiredProps + OwnProps *should* be assignable to P
  return (props: RequiredProps) => (
    // Compiler error here: this literal is not assignable to P
    // We can't even use a type assertion to assert to the compiler that it is RequiredProps + OwnProps, even though as a human we can clearly see that is the case as { bar: 'baz' } satisfies OwnProps and ...rest satisfies what is 'left' in P
    React.createElement(c, { bar: 'baz', ...props })
  )
}

The use case behind this particular pattern is creating a higher-order component that allows a user to specify additional props which are passed through to the child component and is fairly common. :(

@rjdestigter
Copy link

rjdestigter commented Dec 7, 2018

Encountering the same issue as well in my higher order components or functions:

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

interface Foo {
  value: number
}

function withFoo <T extends Foo>(bar: T) {

  type U = Omit<T, keyof Foo>

  return function foo(foo: U): [T, T] {
    const t: T = {
      ...foo,
      value: 42,
    }

    return [t, t]
  }
}

Type '{ value: number; }' is not assignable to type 'T'.

Also tried:

const t: U & Foo = {
      ...foo,
      value: 42,
    }

which outputs:

Type '{ value: number; }' is not assignable to type 'Pick<T, Exclude<keyof T, "value">> & Foo'.
Type '{ value: number; }' is not assignable to type 'Pick<T, Exclude<keyof T, "value">>'.

@sveyret
Copy link

sveyret commented Dec 15, 2018

Same here. The following minimal example does not compile because it cannot find property selected in component Target (method render), even without using a spread rest. This was not the case before upgrading to TS3.2.2

import { Component, ComponentType } from 'react'

interface Props {
  selected: boolean
}

export default function withSelection<P extends Props>(
  Target: ComponentType<P>
) {
  return class Wrapper extends Component {
    public render() {
      return <Target selected />
    }
  }
}

@ahejlsberg ahejlsberg changed the title Constrained generic type acts unconstrained in 3.2.1 Pick<T, Exclude<keyof T, K>> & Pick<T, Extract<keyof T, K>> should be assignable to T Dec 17, 2018
@zheeeng
Copy link

zheeeng commented Dec 18, 2018

I need a correct way to implements providing default options to required options, let them be optional for using-convenience.

like #29062:

interface HasHeight { 
  height: number
}

type Omit<P, K extends keyof P> = Pick<P, Exclude<keyof P, K>>

function makeHeightOptional<P extends HasHeight>(heightRequired: (p: P) => number) {
  return function heightOptional(params: Omit<P, 'height'> & { height?: number }) {
    const height = params.height || 10
    const newParams = { ...params, height }

    // Argument of type 'Pick<P, Exclude<keyof P, "height">> & { height: number; }' is not assignable to parameter of type 'P'.
    return heightRequired(newParams)
  }
}

It's a common and useful high order function.

@sveyret
Copy link

sveyret commented Dec 18, 2018

@ahejlsberg, you changed the title, but in the sample I provided above, this is not the case. The previous title describe more precisely what the problem is, don't you think?

@ahejlsberg
Copy link
Member

ahejlsberg commented Dec 18, 2018

I changed the title of the issue since the core problem is that an intersection of complementary subsets of a higher order type, constructed using Pick<T, K> or by other means, is not assignable back to that higher order type.

Meanwhile, investigating the issue revealed other issues, notably #29067 and #29081. I have fixed both, and with the fixes in place it is now possible to use a type assertion back to the higher order type. For example:

function withDefaults<T, K extends keyof T>(f: (obj: T) => void, defaults: Pick<T, K>) {
    // In the resulting function, properties for which no defaults were provided are required,
    // and properties for which defaults were provided are optional.
    return (obj: Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>) =>
        f({ ...defaults, ...obj } as T);  // Assertion required for now
}

type Item = { name: string, width: number, height: number };

function foo(item: Item) {}

let f1 = withDefaults(foo, { name: 'hi ' });
let f2 = withDefaults(foo, { name: 'hi', height: 42 })

f1({ width: 10 });  // Error, missing 'height'
f1({ width: 10, height: 20 });  // Ok
f2({ width: 10 });  // Ok
f2({ width: 10, height: 20 });  // Ok

If and when we implement the improvement suggested by this issue the as T assertion in the withDefaults function becomes unnecessary.

@rjdestigter
Copy link

Is this issue supposed to be resolved in version 3.3?

@RyanCavanaugh RyanCavanaugh added the Suggestion An idea for TypeScript label Mar 7, 2019
@hronro
Copy link

hronro commented Mar 11, 2019

It seems this still not fixed in v3.4.0-dev.20190310

@Vittly
Copy link

Vittly commented Jun 17, 2019

I did it this way:

// object with property
interface IFoo {
    bar: string;
}

// some React.Component like function 
type TComponent<T> = (param: T) => string;

// global value for "bar" property
const globBar: string = 'lalala';

// function takes bar from global variable and passes it into WrappedComponent
// just like we do this while using React.ContextConsumer
//
// @example
// const MyComponent = withGlobBar(BaseComponent);
// MyComponent({});
export function withGlobBar<P = {}>
    // all magic goes here         ↓↓↓↓↓
    (WrappedComponent: TComponent<P & IFoo>): TComponent<P> {
        return props => WrappedComponent({ ...props, bar: globBar });
}

Hope it helps

@rjdestigter
Copy link

The result of withGlobBar is a component that still requires bar as a prop though rather than returning a wrapped component that now does not require that.

@OliverJAsh
Copy link
Contributor

OliverJAsh commented Jun 19, 2019

I found myself faced with this error in several HOCs, however the workaround of adding a cast wasn't sufficient in all cases. Here's a guide which will hopefully help others facing this issue.

Please correct me if I've got anything wrong.

HOC applies props to composed component

Reduced test case:

import * as React from 'react';
import { ComponentType, FC } from 'react';

type UserProp = { user: string };

const getUser = <ComposedProps extends UserProp>(
    ComposedComponent: ComponentType<ComposedProps>,
) => {
    type Props = Omit<ComposedProps, keyof UserProp>;

    const GetUser: FC<Props> = (props) => {
        /*
        Type '{ user: string; } & Pick<ComposedProps, Exclude<keyof ComposedProps, "user">> & { children?: ReactNode; }' is not assignable to type 'ComposedProps'.
            '{ user: string; } & Pick<ComposedProps, Exclude<keyof ComposedProps, "user">> & { children?: ReactNode; }' is assignable to the constraint of type 'ComposedProps', but 'ComposedProps' could be instantiated with a different subtype of constraint 'UserProp'.
        */
        const composedProps: ComposedProps = {
            user: 'bob',
            ...props,
        };

        /*
    
    */
        return <ComposedComponent {...composedProps} />;
    };

    return GetUser;
};

Workaround this issue by using a cast as suggested in #28884 (comment). If/when this issue is fixed, the cast won't be needed.

        const composedProps: ComposedProps = {
            user: 'bob',
            ...props,
        } as ComposedProps;

HOC receives its own props

Reduced test case:

import * as React from 'react';
import { ComponentType, FC } from 'react';

const requireUser = <ComposedProps,>(
    ComposedComponent: ComponentType<ComposedProps>,
) => {
    type Props = ComposedProps & { user: string };

    const RequireUser: FC<Props> = ({ user, ...composedProps }) => {
        // … do something with `user`;

        /*
        Type 'Pick<PropsWithChildren<Props>, "children" | Exclude<keyof ComposedProps, "user">>' is not assignable to type 'IntrinsicAttributes & ComposedProps & { children?: ReactNode; }'.
            Type 'Pick<PropsWithChildren<Props>, "children" | Exclude<keyof ComposedProps, "user">>' is not assignable to type 'ComposedProps'.
                'ComposedProps' could be instantiated with an arbitrary type which could be unrelated to 'Pick<PropsWithChildren<Props>, "children" | Exclude<keyof ComposedProps, "user">>'.
        */
        return <ComposedComponent {...composedProps} />;
    };

    return RequireUser;
};

This is unsafe because ComposedProps might coincidentally need a prop of the same name as the one our HOC requires (user).

If you're happy to live with unsafe types, you can cast:

        return <ComposedComponent {...composedProps as ComposedProps} />;

A safer solution would be to separate the HOC props from the composed props:

import * as React from 'react';
import { ComponentType, FC } from 'react';

const requireUser = <ComposedProps,>(
    ComposedComponent: ComponentType<ComposedProps>,
) => {
    type Props = { composedProps: ComposedProps; user: string };

    const RequireUser: FC<Props> = ({ user, composedProps }) => {
        // … do something with `user`;

        return <ComposedComponent {...composedProps} />;
    };

    return RequireUser;
};

HOC applies props to composed component and receives its own props

A combination of the above two.

Reduced test case:

import * as React from 'react';
import { ComponentType, FC } from 'react';

type UserProp = { user: string };

const getUserById = <ComposedProps extends UserProp>(
    ComposedComponent: ComponentType<ComposedProps>,
) => {
    type Props = Omit<ComposedProps, keyof UserProp> & { userId: string };

    const GetUserById: FC<Props> = ({ userId, ...restProps }) => {
        /*
        Type '{ user: string; } & Pick<PropsWithChildren<Props>, "children" | Exclude<Exclude<keyof ComposedProps, "user">, "userId">>' is not assignable to type 'ComposedProps'.
            '{ user: string; } & Pick<PropsWithChildren<Props>, "children" | Exclude<Exclude<keyof ComposedProps, "user">, "userId">>' is assignable to the constraint of type 'ComposedProps', but 'ComposedProps' could be instantiated with a different subtype of constraint 'UserProp'.
        */
        const composedProps: ComposedProps = {
            user: 'bob',
            ...restProps,
        };

        return <ComposedComponent {...composedProps} />;
    };

    return GetUserById;
};

This is unsafe because ComposedProps might coincidentally need a prop of the same name as the one our HOC requires and omits (userId).

If you're happy to live with unsafe types, you can cast:

        const composedProps: ComposedProps = ({
            user: 'bob',
            ...restProps,
        } as unknown) as ComposedProps;

A safer solution would be to separate the HOC props from the composed props:

import * as React from 'react';
import { ComponentType, FC } from 'react';

type UserProp = { user: string };

const getUserById = <ComposedProps extends UserProp>(
    ComposedComponent: ComponentType<ComposedProps>,
) => {
    type Props = { composedProps: Omit<ComposedProps, keyof UserProp> } & {
        userId: string;
    };

    const GetUserById: FC<Props> = ({ userId, composedProps }) => {
        const composedPropsComplete = {
            user: 'bob',
            ...composedProps,
        } as ComposedProps;

        return <ComposedComponent {...composedPropsComplete} />;
    };

    return GetUserById;
};

@zacaj
Copy link

zacaj commented Jan 9, 2020

Still no resolution? I think I'm running into this now on 3.7.3 :(

@strblr
Copy link

strblr commented Jan 19, 2020

Please do something for this...

@dragomirtitian
Copy link
Contributor

dragomirtitian commented May 4, 2020

@OliverJAsh To answer your comment on SO yes I think my workaround there could be used in your use case as well:

type UserProp = { user: string };

type ComposeHocProps<TProps, TExtra> = TProps | (Omit<TProps, keyof TExtra> & TExtra)

export const getUserById = <ComposedProps extends UserProp>(
  ComposedComponent: ComponentType<ComposeHocProps<ComposedProps, UserProp>>
) => {
  type Props = { composedProps: Omit<ComposedProps, "user"> } & {
    userId: string;
  };

  const GetUserById: FC<Props> = ({ userId, composedProps }) => {
    return <ComposedComponent {...composedProps} user={userId} />;
  };

  return GetUserById;
};

Playground Link

Not sure if it works in any case, but for this specific case it appears works

frenic added a commit to frenic/glitz that referenced this issue Jul 27, 2020
@hallya
Copy link

hallya commented Aug 14, 2020

Also another example, it worked in my case:

typescript: 3.9.7

import React, { ComponentType, useContext } from 'react';
import { SwitchContext } from './Switch';

type CaseProps<Props extends {}> = {
  id: string;
  component: ComponentType<Props>;
} & Props;

type Remaining<P> = Pick<CaseProps<P>, keyof P>;

export const Case = <Props extends {}>(props: CaseProps<Props>) => {
  const { id, component: Component, ...rest } = props;
  const selected = useContext(SwitchContext);
  
  return id === selected ? <Component {...rest as Remaining<Props>} /> : null;
};

When using Case, props from ComponentA are perfectly inferred by Case.

So saying ComponentA has props typed this way:

type ComponentAProps = {
  foo: string;
}

I'll have to use it this way:

<Case id="id" component={ComponentA} foo="bar" />

playground

@smcjones
Copy link

smcjones commented Dec 7, 2022

Similar syntax, without the Exclude:

Pick<T, K> & Omit<T, K>

Should also work and seems simpler without the Exclude, unless I'm missing something obvious? Entirely possible, but I've tested this and it appears to work and was the bug I came to file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Conditional Types The issue relates to conditional types Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests