Open sandersn opened 8 years ago
@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.
I
gets b
from B
, but in what circumstance would b
not be treated as an own property? In this example, C
doesn't have a
OR b
, so neither does { ... C }
. But that's already how classes work.
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 C
instances are assignable to I
but lose the class inheritance information. Unfortunately this loss of information extends to enumerability as well:
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
specifies a function property but C
provides a method, which is legal in TypeScript. When c
gets spread, though, f
drops out and i.f()
will fail at runtime.
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 ...(typeof MyClass)
supported?
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 undefined
because you can choose to not eager-load all attributes.
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 \
?
You get the instance properties. That's because the spec says that you get the own properties, and instance properties are the closest concept that the compiler tracks.
class C {
static s: number = 12;
i: number = 101;
}
let c = new C();
let spi: { ...C } = { ... c };
spi.i // instance properties
let sps: { ...typeof C } = { ...C };
sps.s; // static properties
partial
type operator.You can add index signatures directly to your spread types:
type UserWhereOptions = { ...User, [attribute: string]: { $and: UserWhereOptions } };
If A and B are sets, then the relative complement of A in B,[1] also termed the set-theoretic difference of B and A,[2] is the set of elements in B but not in A.
The relative complement of A in B is denoted B \ A according to the ISO 31-11 standard. It is sometimes written B - A, but this notation is ambiguous, as in some contexts it can be interpreted as the set of all elements b - a, where b is taken from B and a from A.
https://en.wikipedia.org/wiki/Complement_(set_theory)
What speaks against \
is that a lot of languages use -
as an operator, not everyone is familiar with set theory, backslash has a meaning of "escaping" stuff and JavaScript is known for abusing arithmetic operators already (concatenation should really be .
, not +
).
But maybe we can at least not call it subtraction type, but difference type in the proposal (you wouldn't call a string concatenation a string addition, just because it uses the +
)
(4) Good idea. Done.
I think it would be a better idea to use the syntax flowtype uses (#2710), i.e. $Diff<A, B>
instead of -
or \
. There are other issues like #4889 where we can use the same syntax, i.e. $Partial<T>
or $Shape
, instead of introducing a new syntax for each case.
diff<A,B>
, partial<T>
or shape<T>
would be ok as well.
I'm -1 on the flow syntax:
I updated the proposal to use a binary syntax A ... B
. This makes the specification simpler. TypeScript will continue to use object type literal syntax to specify spread types: { ...T, ...U }
instead of T ... U
.
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.
We would like to pursue mapped types to model the type space for these changes.
Hi all,
Apologises if this is the wrong issue to to raise this question; I'll move as necessary if it is.
However, I've been tracking this work with keen interest and was wondering if there will be a similar type for Array or Tuple types. I know #5453 tracks something similar, although this seems to be specifically for function calls (which sadly seems blocked due to complexities arising from the various use-cases).
Essentially, the use-case I'm looking to solve is the ability to type an Array/Tuple where the first n-elements are typed, and then the remaining elements are typed as an unbounded list. To be more precise (and correct my loose language), consider:
type AT_LEAST_ONE_ELEMENT = [string, ...string]
// To support the case where a CSV row is parsed, and the first 4 columns are known, but the remaining columns (if any) will be strings
type FLEXIBLE_CSV_ARRAY = [string, number, string, boolean, ...string]
For a few bonus points, it would be great if intersections worked like:
type ONE_ELEMENT = [string]
type MANY_ELEMENTS = string[]
type AT_LEAST_ONE_ELEMENT = ONE_ELEMENT & MANY_ELEMENTS // === [string ...string]
Object spread types work with objects, and this proposal sounds more like a modification of tuples to add an array spread type for working with tuples. A lot like #6229, in fact, "Strict and open length tuples", but adding to that proposal to allow the specification of an array-spread type at the end.
I can't quite read from the proposal if I'd be able to do:
type A = {
x: string;
y: string;
}
type B = {
y: string;
}
// C would only contain x
type C = rest(A, B);
Of if the last arguments to rest
have to be explicit property names?
Almost; currently it's rest(A, keyof B)
with the restriction that B has to be an object type. It can't be a type parameter.
The updated proposal is with the PR at #13470. I need to update this issue to match it.
There's a hack that allows to build the spread type:
interface A {
method(): void;
prop: number;
}
interface B {
anotherMethod(): void;
anotherProp: number;
}
const spread = {...({} as A & B)};
type ABSpread = typeof spread;
declare const s: ABSpread;
s.prop; // ok
s.method; // error
s.anotherProp; // ok
s.anotherMethod; // error
@sandersn Is it going to land anytime soon?
Do I understand correctly that this should allow to have normally merged object types:
type A = { x: string, y: string }
type B = { x: number, z: number}
type C = {...A, ...B} // {x: number, y: string, z: number}
?
It's not on our immediate list of things to ship.
You are correct, if you spread two object types, you get a new object type instead of a spread type.
Is this what's causing this code to have no issues? I would expect an error when setting bar
in doSomething
.
interface MyObj {
foo?: number,
}
function doSomething(state: MyObj): MyObj {
return { ...state, bar: 42 }
}
Nope, there are no generic types in your code. That is just a legal spread. Objects with spreads don't check for excess properties. #12997 (some design notes) has some explanation for this at the end.
Will having spread types allow for excess property checking in spreads?
That is not related.
@Kovensky #12936 tracks exact types, which is the feature you want.
Does this PR support the use case shown here? https://github.com/tc39/proposal-object-rest-spread/issues/45
Basically conditional assignment of object keys using object spread. I see that "Null/Undefined Are Ignored", but does that extend to false
as well?
Currently, the emitted Javascript is correct but the compiler issues an error. We didn't think of the pattern you linked to, and thought that people would like to know if they mistakenly spread a boolean where they thought they had an object type.
We could obviously remove the error if lots of people use this pattern. I recommend that you create a new issue, label it Suggestion, and try to get upvotes for it. That's typically how we gather evidence for a change.
Syntax support aside, I think we could type the operations covered by this syntax without special types:
Omit
(#12215)Overwrite
(#12215), basically MergeAll
if the compiler can extract syntax { a, b, ...rest }
into e.g. [{ a, b }, rest]
or [{ a }, { b }, rest]
Still happening in 2.6.2...
@lednhatkhanh is that a question or a statement? I'd like to know if the spread operator is coming too..
Hi @mhegazy , @sandersn . Is there any update or good news for this issue ? I see only @mhegazy changes milestones in past months . Thanks
@burabure This issue is specifically about type spreads, I believe.
What you're doing should already work with the spread operator, I think.
@Dru89 I'm such a dummy, I confused the issue XD
This is still happening as of TypeScript 2.8.3.
Hi, what is the status of it?
I would really love to have:
function merge<A, B>(a: A, b: B): A {
return { ...a, ...b};
}
So that the compiler typechecks if the object of type B can be merged into A.
@mshoho You mean function merge<A, B>(a: A, b: B): A & B
?
No. Something like:
interface AA {
prop1: string;
prop2?: number;
prop3?: boolean;
}
interface BB {
prop2?: number;
prop3?: boolean;
}
interface CC {
prop2?: number;
prop4?: string;
}
function merge<A, B>(a: A, b: B): A {
return { ...a, ...b };
}
let a: AA = { prop1: 'hello', prop2: 1 };
let b: BB = { prop2: 100, prop3: true };
let c: CC = { prop2: 500, prop4: 'world' };
merge(a, b); // ok.
merge(a, c); // compiler error.
@mshoho you can approximate this with conditional types, only returning a "useful" type in the event that the second argument is compatible with the first:
function merge<A, B>(a: A, b: B): Required<A> extends Required<B> ? A & B : {} {
return Object.assign({}, a, b) as any;
}
merge(a, b).prop2; // ok.
merge(a, c).prop2; // compiler error.
@pelotom I have something like this:
function someExternalFunction(a: AA) {
// Something external, out of my control.
}
// And I need safely typechecked way to:
someExternalFunction(merge(a, b)); // ok.
someExternalFunction(merge(a, c)); // compiler error.
Unfortunately, {} | AA is not assignable to AA.
@mshoho did you try what I wrote? The result type is not a union...
@pelotom yes, I've just tried it.
Perhaps because I have a chain of functions with generics:
function process<A, C>(a: A, c: C) {
someExternalFunction(merge(a, c));
}
I get this for the merge call:
[ts]
Argument of type 'Required<A> extends Required<C> ? A : {}' is not assignable to parameter of type 'AA'.
Type '{} | A' is not assignable to type 'AA'.
Type '{}' is not assignable to type 'AA'.
Property 'prop1' is missing in type '{}'.
@mshoho ah, yeah, that'd do it. If you don't know what A
and C
are, I don't think there's a way to ensure they are compatible.
@pelotom hence my question about the status. Because as far as I understand, my problem will be automagically fixed by this ticket.
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.