-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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 spread expressions in object literals #28234
Conversation
Awesome, this is my most anticipated feature! One thing I don't quite understand is how will this work with rest spread operator? For example what inferred return type this function will have?
|
@hatamov This PR doesn't change the behavior of rest properties. However, I'm considering a separate PR that would use the function foo<T extends { a: string, b: string }>(obj: T) {
let { a, b, ...rest } = obj; // let rest: Pick<Exclude<T, 'a' | 'b'>>
} |
@ahejlsberg Would it be possible to use function spread<T, U>(t: T, u: U) {
return { ...t, ...u }; // Pick<T, Exclude<keyof T, keyof U>> & U
} Of course this needs to be a little bit more complex to correctly handle optional properties. |
It's possible, see for example my comment here. However, it ends up generating very complex types (and commensurately complex error messages), and even then we still lack accurate representation of which properties were declared optional and which properties are "own enumerable" properties. |
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.
- Test cleanup
- Higher-order rest should be an error until we address it too.
@sandersn Your comments should be addressed now. In some cases I moved the tests, in others I just updated the comments. |
@@ -17697,9 +17718,8 @@ namespace ts { | |||
} | |||
|
|||
function isValidSpreadType(type: Type): boolean { | |||
return !!(type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.NonPrimitive) || | |||
return !!(type.flags & (TypeFlags.AnyOrUnknown | TypeFlags.NonPrimitive | TypeFlags.Object | TypeFlags.InstantiableNonPrimitive) || |
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.
In this InstantiableNonPrimitive
case, shouldn't this be checking that the constraint of type
, if it exists, isValidSpreadType
?
Is the following behaviour also grouped under this description? function ripoff<T>(augmented: T): T {
const { ...original } = augmented;
return original;
}
interface Foo {
x: number;
(y: string): number;
}
const foo = (x: string) => x.length;
foo.x = 42;
const x: Foo = ripoff(foo);
x("string") // TypeError: x is not a function Did the team consider returning the type |
@jack-williams Yes, except that's rest, not spread. The principle is the same though. We think that pattern of spreading a callable object is rare, especially in generic functions like |
Yep sorry, silly mistake.
I agree that the callable case is rare (and contrived) but there are other object invariants someone might expect to be preserved by a function of type I expect my concerns are unfounded, it just seems that types like |
Is this expected? Playground function func2<T extends { a: string, b: string }>(param: T)
: { a: string, b: string } {
const { a, ...rest } = param;
const x = rest.b; // Property 'b' does not exist on type 'Pick<T, Exclude<keyof T, "a">>'.
return { a, ...rest }; // Property 'b' is missing in type '{ a: string; } & Pick<T, Exclude<keyof T, "a">>'
// but required in type '{ a: string; b: string; }'.
} |
@JasonKleban That was fixed by #29121, give it a try with |
A bit of an aside to Anders' comment:
I was looking at typing Given:
If we wanted to type
The library
The generated result type looks pretty bad: To improve it, we can handle the "exclude everything" case like so:
This greatly simplifies the generated type: However, if we add back more properties...
We are again in "scary type" territory: To improve this we can inline
And we end up with something a bit nicer (although there seems to be no way to force I'm not sure if this is worth doing to |
interface InputProps {
name: string
value: any
}
interface SelectProps extends InputProps{
items: any[]
}
export const Input = (props : InputProps) => {
return <div>Sono un input</div>
}
export const Select = (props : SelectProps) => {
return <div>Sono un Select</div>
}
type PropsOf<T> = T extends React.ComponentType<infer Props> ? Props : object
// type BPropsOf<T> = PropsOf<BaseFieldProps<T>>
type BaseFieldProps<T> = {
field1: string
field2: boolean
component: React.ComponentType<PropsOf<T>>
} & {
[Property in keyof PropsOf<T>]: PropsOf<T>[Property]
}
// type FieldProps = BaseFieldProps<InputProps> // & PropsOf<BaseFieldProps<InputProps>['component']>
// & PropsOf<FieldProps['component']>
function Field < C >(props : BaseFieldProps<C>){
return <div>Sono un field</div>
}
export const Home = () => {
return (
<div>
<h2>I am body</h2>
<Field<typeof Select>
field1="field1"
field2
component={Select}
{/* Here all the properties of the component */}
/>
</div>
)
} There is a way to avoid having to write the generic "typeof Select" near "Field" and extract it from the prop "component"?? |
…,Config}` - Previous code is inconsistent with jsdoc description: "Both initial state and initial configuration options are merged with defaults upon initialization." - Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234
…,Config}` - Previous code is inconsistent with jsdoc description: "Both initial state and initial configuration options are merged with defaults upon initialization." - Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234
…,Config}` - Previous code is inconsistent with jsdoc description: "Both initial state and initial configuration options are merged with defaults upon initialization." - Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234
- Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234
- Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234
## Explanation - For runtime property assignment, use `as unknown as` instead of `as any`. - Change the types for `BaseControllerV1` class fields `initialConfig`, `initialState` from `C`, `S` to `Partial<C>`, `Partial<S>`. - Initial user-supplied constructor options do not need to be complete `C`, `S` objects, since `internal{Config,State}` will be populated with `default{Config,State}`. - For empty objects, prefer no type assertions or `as never` (`never` is assignable to all types). - Fix code written based on outdated TypeScript limitation. - Generic spread expressions for object literals are supported by TypeScript: microsoft/TypeScript#28234 ## References - Closes #3715 ## Changelog ### [`@metamask/base-controller`](https://github.com/MetaMask/core/pull/3959/files#diff-a8212838da15b445582e5622bd4cc8195e4c52bcf87210af8074555f806706a9) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate
With this PR we permit generic types in spread expressions within object literals. Object literals with generic spread expressions now produce intersection types, similar to the
Object.assign
function and JSX literals. For example:Property assignments and non-generic spread expressions are merged to the greatest extent possible on either side of a generic spread expression. For example:
Non-generic spread expressions continue to be processed as before: Call and construct signatures are stripped, only non-method properties are preserved, and for properties with the same name, the type of the rightmost property is used. This contrasts with intersection types which concatenate call and construct signatures, preserve all properties, and intersect the types of properties with the same name. Thus, spreads of the same types may produce different results when they are created through instantiation of generic types:
An alternative to using intersections would be to introduce a higher-order type operator
{ ...T, ...U }
along the lines of what #10727 explores. While this appears attractive at first, it would take a substantial amount of work to implement this new type constructor and endow it with all the capabilities we already implement for intersection types, and it would produce little or no gain in precision for most scenarios. In particular, the differences really only matter when spread expressions involve objects with overlapping property names that have different types. Furthermore, for an unconstrained type parameterT
, the type{ ...T, ...T }
wouldn't actually be assignable toT
, which, while technically correct, would be pedantically annoying.Having explored both options, and given that intersection types are already used for
Object.assign
and JSX literals, we feel that intersection types strike the best balance between accuracy and complexity for this feature.