-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Generic object rest variables and parameters #28312
Conversation
# Conflicts: # tests/baselines/reference/objectRest.errors.txt # tests/baselines/reference/objectRest.types
Would it be worth considering adding |
We considered that, but there are too many projects out there that already define their own |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a noLib
test would be nice, but it looks good.
@@ -34,8 +33,6 @@ tests/cases/conformance/types/rest/objectRestNegative.ts(17,9): error TS2701: Th | |||
~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SHould probably remove this function generic<T ...
example since the new test covers exactly the same case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but no harm in keeping it.
interface I { | ||
[s: string]: boolean; | ||
[s: number]: boolean; | ||
} | ||
|
||
var o: I = { | ||
~ | ||
!!! error TS2322: Type '{ [x: string]: string | number; }' is not assignable to type 'I'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any idea what's going on here? I'm not sure why we stopped issuing the per-property error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed it was the change in looking up the type of property names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because we previously treated all computed properties as having known (unit type) names, when here they actually have type string
. Since they now have type string
we generate a string index signature, and the elaboration logic stops trying to drill down to the properties.
# Conflicts: # src/compiler/checker.ts
Is this scenario is not supported? Or this is a bug? function foo<T>(obj: T & { x: string }): T {
const { x, ...rest } = obj;
return rest; // Error: [ts] Type 'Pick<T & { x: string; }, Exclude<keyof T, "x">>' is not assignable to type 'T'. [2322]
} |
@vkrol if |
@ajafff But |
@vkrol Let @ajafff While this is correct, I think it’s actually undesired. Most of the motivation for the higher-order definition of spread as intersection was that it’s quite usable, as well as correct [1] for the common case where two disjoint types are spread together (the case here) or the same type is spread into itself. The definition of rest we ended up with is actually more correct, as you note, but it means that it doesn’t have the same assignability behaviour as spread. I think the correct fix here would be to add a simplification rule in assignability checking that says that However, I’m not sure if such a rule would be applicable anywhere else, and I think it would be fiddly to get right since [1] except of course for own-ness, which the compiler doesn’t track well anyway. |
What about following scenario @sandersn ( related to react HoC, but the same behaviour happens with raw functions ) Fix: It needs to be casted to original So is this correct behaviour or a bug ? |
As far as I understand the problem, I think it's the same as @vkrol's. I'm not sure I understand it, however. If InjectedProps is the type that's related to withCounter, why does counterWannabe have to declare it as a type at all? Shouldn't In other words, why does Of course, given the |
@sandersn I wanted to simplify the example by not involving react but I guess that was bad move :D sorry about that. So here is the React HoC example, with comments: update 11/22/18:
import React, { Component } from 'react'
import { Counter } from './counter-render-prop'
import { Subtract } from '../types'
type ExtractFuncArguments<T> = T extends (...args: infer A) => any ? A : never;
// get props that Counter injects via children as a function
// InjectedProps is gonna be:
// { count: number; } & { inc: () => void; dec: () => void; }
type InjectedProps = ExtractFuncArguments<Counter['props']['children']>[0];
// withCounter will enhance returned component by ExtendedProps
type ExtendedProps = { maxCount?: number };
// P is constrained to InjectedProps as we wanna make sure that wrapped component
// implements this props API
const withCounter = <P extends InjectedProps>(Cmp: React.ComponentType<P>) => {
class WithCounter extends Component<
// enhanced component will not include InjectedProps anymore as they are injected within render of this HoC and API surface is gonna be extended by ExtendedProps
Subtract<P, InjectedProps> & ExtendedProps
> {
static displayName = `WithCounter(${Cmp.name})`;
render() {
const { maxCount, ...passThroughProps } = this.props;
return (
// we use Counter which has children as a function API for injecting props
<Counter>
{(injectedProps) =>
maxCount && injectedProps.count >= maxCount ? (
<p className="alert alert-danger">
You've reached maximum count! GO HOME {maxCount}
</p>
) : (
// here cast to as P is needed otherwise compile error will occur
<Cmp {...injectedProps} {...passThroughProps as P} />
)
}
</Counter>
);
}
}
return WithCounter;
};
// CounterWannabe implement InjectedProps on it's props
class CounterWannabe extends Component<
InjectedProps & { colorType?: 'primary' | 'secondary' | 'success' }
> {
render() {
const { count, inc, colorType } = this.props;
const cssClass = `alert alert-${colorType}`;
return (
<div style={{ cursor: 'pointer' }} className={cssClass} onClick={inc}>
{count}
</div>
);
}
}
// if CounterWannabe would not implement InjectedProps this line would get compile error
const ExtendedComponent = withCounter(CounterWannabe); |
@Hotell What's the source of |
I read the intro to HOCs and I think I understand what's going on, although I haven't had a chance to play with the whole example without Counter.
|
Sorry about that, it's not really important I should explicitly provide props in that example. 👉 I've updated previous comment type State = typeof initialState
type Props = {
children: (
props: State & { inc: Counter['handleInc']; dec: Counter['handleDec'] }
) => React.ReactChild
count?: number
} & typeof defaultProps
const initialState = { count: 0 }
const defaultProps = {
onChange: (value: number) => {}
}
export class Counter extends Component<Props, State> {
static defaultProps = defaultProps
render(){ /*...*/ }
}
My bad, updated the code above
will produce the same error
Thanks a lot @sandersn 💪 |
With this PR we permit rest properties in destructurings of objects of generic types. This effectively implements what is suggested in #10727 (although by different means) and complements our support for generic spread expressions in object literals implemented in #28234.
When a destructuring of an object of a generic type includes a rest variable, the type of the rest variable is an instantiation of the
Pick
andExclude
predefined types:Some additional examples: