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?
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 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. :(
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">>'.
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 />
}
}
}
I need a correct way to implements providing default options to required options, let them be optional for using-convenience.
like https://github.com/Microsoft/TypeScript/issues/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.
@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?
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.
Is this issue supposed to be resolved in version 3.3?
It seems this still not fixed in v3.4.0-dev.20190310
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
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.
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.
Workaround this issue by using a cast as suggested in https://github.com/microsoft/TypeScript/issues/28884#issuecomment-448356158. If/when this issue is fixed, the cast won't be needed.
import * as React from "react";
import { ComponentType, FC } from "react";
type UserProp = { user: string };
export const getUser = <ComposedProps extends UserProp>(
ComposedComponent: ComponentType<ComposedProps>
) => {
type Props = Omit<ComposedProps, "user">;
const GetUser: FC<Props> = props => {
const composedProps = {
user: "bob",
...props
};
/*
Type '{ user: string; } & Pick<ComposedProps, Exclude<keyof ComposedProps, "user">> & { children?: ReactNode; }' is not assignable to type 'IntrinsicAttributes & ComposedProps & { children?: ReactNode; }'.
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'.ts(2322)
*/
return <ComposedComponent {...composedProps} />;
};
return GetUser;
};
In this case the cast workaround from above won't work.
import * as React from "react";
import { ComponentType, FC } from "react";
type UserProp = { user: string };
export const getUserById = <ComposedProps extends UserProp>(
ComposedComponent: ComponentType<ComposedProps>
) => {
type Props = Omit<ComposedProps, "user"> & { userId: string };
const GetUserById: FC<Props> = ({ userId, ...restProps }) => {
// Type error, even with the cast
// Technically unsafe, because composed props might need `userId`
/*
Conversion of type '{ user: string; } & Pick<PropsWithChildren<Props>, "children" | Exclude<Exclude<keyof ComposedProps, "user">, "userId">>' to type 'ComposedProps' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
'{ 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'.ts(2352)
*/
const composedProps = {
user: "bob",
...restProps
} as ComposedProps;
return <ComposedComponent {...composedProps} />;
};
return GetUserById;
};
This is unsafe because composed props 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 tweak the cast:
const composedProps = {
user: 'bob',
...restProps,
- } as ComposedProps;
+ } 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 };
export const getUserById = <ComposedProps extends UserProp>(
ComposedComponent: ComponentType<ComposedProps>
) => {
type Props = { composedProps: Omit<ComposedProps, "user"> } & {
userId: string;
};
const GetUserById: FC<Props> = ({ userId, composedProps }) => {
const composedPropsComplete = {
user: "bob",
...composedProps
} as ComposedProps;
return <ComposedComponent {...composedPropsComplete} />;
};
return GetUserById;
};
Still no resolution? I think I'm running into this now on 3.7.3 :(
Please do something for this...
@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;
};
Not sure if it works in any case, but for this specific case it appears works
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" />
Most helpful comment
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:
If and when we implement the improvement suggested by this issue the
as T
assertion in thewithDefaults
function becomes unnecessary.