-
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
Add spread/rest higher-order types operator #10727
Comments
@ahejlsberg @RyanCavanaugh can you take a look at the spread type section and see if there's something I forgot to take into account? The rest type section isn't really done because I want to talk about the problems in person. |
You mention "own" properties; the type system today does not have any definition for this, and you do not define it clearly. I would say we should just ignore this for now. |
Defining 'own' as 'not inherited' gets us pretty close, since we have a good definition of inherited already. Actually, if I recall, the PR implements 'enumerable' as 'things that are not methods', which is pretty close in a similar way. There will still be false positives, but not from ES6-style class hierarchies. |
Interfaces are the issue. it is common for ppl to define interfaces as hierarchy to share declarations. so are these "own" or not? |
I'm having trouble coming up with an example. Here's what I got so far. interface B {
b: number
}
interface I extends B {
a: number
}
class C implements I { // error, C not assignable to I
c: number
}
let ib: { ...C } = { ...new C() }
let ib2: { a: number, b: number, c: number} = ib; // error, C and { ...C } don't have a or b.
If you have an interface hierarchy that matches the class hierarchy, then inheritance still works as a definition for own types: class K implements B {
b: number
}
class C extends K implements I {
a: number
}
let a: { ... C } = { ... new C() }
let ab: { a: number, b: number } = a; // error, { ... C } doesn't have b. |
I meant speading a value whose tupe is an interface: interface B {
b: number
}
interface I extends B {
a: number
}
var i: I;
var x = {...i}; // is x {a: number} or {a:number, b:number}? |
@DanielRosenwasser and I came up with some counterexamples for both own and enumerable properties. They are below. Basically, since we don't track this information in the type system, we need either
I'm not sure how hard (1) is. interface I {
a: number;
b: number;
}
class K {
b = 12
}
declare class C extends K implements I {
a = 101
}
let c: I = new C();
let i: I = { ...c } // no error, but should be because 'b' was missed
i.b // error at runtime: 'b' was not an own property and got removed. The missing piece in this example is that interface I {
a: number;
f: () => void;
}
class C implements I {
a = 12;
f() { };
}
let c: I = new C();
let i: I = { ... c }; // no error, but should be because f is missed
i.f() // error at runtime: f was not an enumerable property In this example, |
I updated the proposal to not specify own, enumerable properties. I should add a note that we deviate from the stage 2 spec, though. |
Subtraction types would really be very nice to have. |
For the subtraction type example, shouldn't it be error? /** Typescript version */
function removeX<T extends { x: number, y: number }>(o: T) {
let { x, ...rest }: T - { x: number } = o; // Error?: Type "T - { x: number }" has no property "x"
return rest;
}
// Did you intend this?
function removeX<T extends { x: number, y: number }>(o: T): T - { x: number } {
let { x, ...rest } = o;
return rest;
} |
Yes, thanks. I'll update the proposal. |
This is pretty awesome! One thing that is not clear to me: What will spreading a class do? Will it take the instance properties or the static properties? I would say the first, so to get the static properties is spreading like The use case is as follows: methods/constructors that take an object literal as an argument, for example Sequelize (ORM): class User extends Model {
public id?: number;
public name?: string;
constructor(values?: ...User);
static update(changes: ...User): Promise<void>;
static findAll(options?: {where?: ...User}): Promise<User[]>;
} What is limiting here of course still is that there is no way to mark all the properties as optional, but in the case of Sequelize all properties can be It would also be nice to know if a union type of a spread type and an index signature type is allowed: type UserWhereOptions = ...User & {
[attribute: string]: { $and: UserWhereOptions } | { $or: UserWhereOptions } | { $gt: number };
}
class User extends Model {
static findAll(options?: {where?: UserWhereOptions}): Promise<User[]>;
} Regarding subtraction types, isn't a subtraction a mathematical operation on numbers? Shouldn't this really be the difference operator |
|
https://en.wikipedia.org/wiki/Complement_(set_theory) |
(4) Good idea. Done. |
I think it would be a better idea to use the syntax flowtype uses (#2710), i.e.
|
I'm -1 on the flow syntax:
|
I updated the proposal to use a binary syntax |
I updated the proposal to reflect recent changes in the PR. Specifically, assignability got stricter, spreads are no longer type inference targets, and index signatures only spread if both sides have an index signature. |
Just want to make sure that I'm not missing something. To be clear the PRs referenced, merged, and released above added support for object spread, but did not add support for type spread (as noted in the original post). So today, this is not supported: let ab: { ...A, ...B } = { ...a, ...b }; Is that correct? Today, I'm using this handy utility I copy/pasted from stackoverflow: // Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
{ [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];
// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
{ [P in K]: L[P] | Exclude<R[P], undefined> };
type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*
// Type of { ...L, ...R }
type Spread<L, R> = Id<
// Properties in L that don't exist in R
& Pick<L, Exclude<keyof L, keyof R>>
// Properties in R with types that exclude undefined
& Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;
type A = {bool: boolean, str: boolean}
type B = {bool: string, str: string}
type C = Spread<A, B>
const x: C = {bool: 'bool', str: 'true'}
console.log(x) Is there a better way to accomplish this today? I'm guessing that's what this issue is intended to address, but I'm happy to open a new one if I'm missing something. Thanks! |
Yes. We never found the balance of correctness and usability for spread types that justified their complexity. At the time, React higher-order components also required negated types in order to be correctly typed. Finally, we decided that since we'd been using intersection as a workaround for so long, that it was good enough. Just glancing at your |
This is a very handy functionality to not having to write that many types. Flow allows this so it is doable. If the original proposition is too complex, maybe a more limited one may be implemented, but just ignoring it is not the way to go IMO |
@kentcdodds Not sure it works for all cases but shorter type IntersectPropToNever<A, B> = {
[a in keyof (A & B)]: (A & B)[a]
}
type Spread<A, B> = IntersectPropToNever<A, B> & A | B;
type A = {a: string, b: string};
type B = {b: number};
const tt: Spread<A,B> = {a: '', b: 1};
const ee: Spread<B,A> = {a: '', b: '1'};
// error Type 'string' is not assignable to type 'number'.
const tt_err: Spread<A,B> = {a: '', b: '1'};
// error Type 'number' is not assignable to type 'string'.
const ee_err: Spread<B,A> = {a: '', b: 1};
// Property 'b' is missing in type '{ a: string; }'
const ww_err: Spread<A,B> = {a: ''} |
I'll share a very specific example of how this could be useful. With the Cypress end-to-end testing framework you register global commands with So let's say you have a export function myCommand1(selector: string) {
// ...
}
export function myCommand2(index: number) {
// ...
} Then in an declare namespace Cypress {
import * as commands from './commands';
interface Chainable {
myCommand1: typeof commands.myCommand1;
myCommand2: typeof commands.myCommand2;
}
} But it would be nice if I could just add all my functions implicitly at once: declare namespace Cypress {
import * as commands from './commands';
interface Chainable {
...(typeof commands)
}
} That way I can register new commands by just adding a new exported function to |
This comment was marked as outdated.
This comment was marked as outdated.
@istarkov's solution is incomplete. Take the following example: type IntersectPropToNever<A, B> = {
[a in keyof (A & B)]: (A & B)[a]
}
type Spread<A, B> = IntersectPropToNever<A, B> & A | B;
type Test = Spread<{
a: true
}, {
a?: false
}>
|
Um... I guess everyone else understands this given there are hundreds of upvotes and I'm the only 😕, but why does one need to "convert the syntax to binary syntax"? The obvious syntax is like type A = { a:number, b?: number };
type B = { b:string };
let ab: { ...A, ...B } = { ...{ a: 1, b: 2 }, ...{ b: 'hi' } }; I was imagining that maybe binary |
Since this has been marked as duplicate of #11100 I have to comment here, even though it's unrelated to spread operator. What is the reason for |
The spread type is a new type operator that types the TC39 stage 3 object spread operator. Its counterpart, the difference type, will type the proposed object rest destructuring operator. The spread type
{ ...A, ...B }
combines the properties, but not the call or construct signatures, of entities A and B.The pull request is at #11150. The original issue for spread/rest types is #2103. Note that this proposal deviates from the specification by keeping all properties except methods, not just own enumerable ones.
Proposal syntax
The type syntax in this proposal differs from the type syntax as implemented in order to treat spread as a binary operator. Three rules are needed to convert the
{ ...spread1, ...spread2 }
syntax to binary syntaxspread1 ... spread2
.{ ...spread }
becomes{} ... spread
.{ a, b, c, ...d}
becomes{a, b, c} ... d
{ a, b, c, ...d, ...e, f, g}
becomes{a, b, c} ... d ... e ... { f, g }
.Type Relationships
A ... A ... A
is equivalent toA ... A
andA ... A
is equivalent to{} ... A
.A ... B
is not equivalent toB ... A
. Properties ofB
overwrite properties ofA
with the same name inA ... B
.(A ... B) ... C
is equivalent toA ... (B ... C)
....
is right-associative.|
, soA ... (B | C)
is equivalent toA ... B | A ... C
.Assignment compatibility
A ... B
is assignable toX
if the properties and index signatures ofA ... B
are assignable to those ofX
, andX
has no call or construct signatures.X
is assignable toA ... B
if the properties and index signatures ofX
are assignable to those ofA ... B
.Type parameters
A spread type containing type parameters is assignable to another spread type if the type if the source and target types are both of the form
T ... { some, object, type }
and both source and target have the same type parameter and the source object type is assignable to the target object type.Type inference
Spread types are not type inference targets.
Properties and index signatures
In the following definitions, 'property' means either a property or a get accessor.
The type
A ... B
has a propertyP
ifA
has a propertyP
orB
has a propertyP
, andA.P
orB.P
is not a method.In this case
(A ... B).P
has the typeB.P
ifB.P
is not optional.A.P | B.P
ifB.P
is optional andA
has a propertyP
.A.P
otherwise.private
,protected
andreadonly
behave the same way as optionality except that ifA.P
orB.P
isprivate
,protected
orreadonly
, then(A ...B).P
isprivate
,protected
orreadonly
, respectively.Index signatures
The type
A ... B
has an index signature ifA
has an index signature andB
has an index signature. The index signature's type is the union of the two index signatures' types.Call and Construct signatures
A ... B
has no call signatures and no construct signatures, since these are not properties.Precedence
Precedence of
...
is higher than&
and|
. Since the language syntax is that of object type literals, precedence doesn't matter since the braces act as boundaries of the spread type.Examples
Taken from the TC39 proposal and given types.
Shallow Clone (excluding prototype)
Merging Two Objects
Overriding Properties
Default Properties
Multiple Merges
Getters on the Object Initializer
Getters in the Spread Object
Setters Are Not Executed When They're Redefined
Null/Undefined Are Ignored
Updating Deep Immutable Object
Note: If
A = { name: string, address: { address, zipCode: string }, items: { title: string }[] }
, then the type of newVersion is equivalent toA
Rest types
The difference type is the opposite of the spread type. It types the TC39 stage 3 object-rest destructuring operator. The difference type
rest(T, a, b, c)
represents the typeT
after the propertiesa
,b
andc
have been removed, as well as call signatures and construct signatures.A short example illustrates the way this type is used:
Type Relationships
rest(A)
is not equivalent toA
because it is missing call and construct signatures.rest(rest(A))
is equivalent torest(A)
.rest(rest(A, a), b)
is equivalent torest(rest(A, b), a)
andrest(A, a, b)
.rest(A | B, a)
is equivalent torest(A, a) | rest(B, a)
.Assignment compatibility
rest(T, x)
is not assignable toT
.T
is assignable torest(T, x)
becauseT
has more properties and signatures.Properties and index signatures
The type
rest(A, P)
removesP
fromA
if it exists. Otherwise, it does nothing.Call and Construct signatures
rest(A)
does not have call or construct signatures.Precedence
Difference types have similar precedence to
-
in the expression grammar, particularly compared to&
and|
. TODO: Find out what this precedence is.The text was updated successfully, but these errors were encountered: