You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note 1: PickDefined<T> is necessary with strictNullChecks enabled. Note 2: In the definition of Assign<A, B> I would have expected the usage of keyof B to need to be replaced with keyof PickDefined<B>. It seems, that's presently not the case - so I've opted to omit it and avoid any potential slow-down. Nonetheless, I must admit that I'm hazy as to why this is permissible.
Obviously to keep things brief I've just covered the single source use-case rather than providing overloads with several source. The definition is nonetheless chainable i.e. Assign<A, Assign<B, C>> etc.
Relevant comment (in #28553) saying why this is not already being done:
Spreading two overlapping types is pretty rare, so we decided not to create a type operator for it.
Relevant possible implementation (in #21316) of a Spread<L, R> which is similar to Assign<A, B>. I don't know if anyone has a demonstrably superior implementation of this thing given all the potential edge cases like unknown, optional, and non-own properties:
classA{c?: boolean;constructor(publica: string){}sayA(){console.log(this.a);}}classBextendsA{constructor(a: string,publicb: string){super(a);}}functionhmm(a: A){returnassign({b: 123,c: 456},a);}constresult=hmm(newB("a","b"));// unknown propertiesresult.b;// number at compile time, but:console.log(typeofresult.b);// "string"// optional propertiesresult.c;// boolean | undefined at compile time, but:console.log(typeofresult.c);// "number"// non-own propertiesresult.sayA;// function at compile time, but:console.log(typeofresult.sayA);// "undefined"
Spreading two overlapping types is pretty rare, so we decided not to create a type operator for it.
It's bit of a shame as there's been a heap of improvements in soundness recently, really loving the direction!
Not that I think it's particularly compelling on its lonesome, but just for reference, I am seeing overlapping types being assigned. Specifically custom "sub-classes", default props of number[] being overwritten with string[], Date[] etc. Although, in the aforementioned case it's actually with a custom recursive assign in a JS library (chartist). I was improving the DefinitelyTyped definitions for chartist when I noticed Object.assignseemingly wasn't up to scratch.
Anyway, I do totally understand the decision though. I guess it'd only be feasible with first class support for "exact types" (#12936), and my head is hurting just thinking about how much that'd further complicate the types here.
I also noted that when chained my Assign isn't propagating the indexer; technically it needs to create a new indexer with a union of the return types in cases of:
letx: numberObject.assign({},"abcde",[1,2,3])[x]// string | number
Hopefully this will become a bit more feasible in near future; I'm a sucker for soundness.
For future reference, whilst without a doubt it's far superior to my own attempt, Anders Hejlsberg (@ahejlsberg)'s implementation unfortunately does have a couple of failures when run against my test suite. Failing test1 in particular seems non-ideal. However, just to be clear, Anders Hejlsberg (@ahejlsberg) does a much better job of dealing with prickly edge cases (i.e. those mentioned above).
EDIT: It's quite conceivable that I can't just naively apply the Spread type to my assign, and that's responsible for 2 failed tests.
You've got --strictNullChecks disabled there (well, the Playground does that by default), and therefore the inferred type of {a: "one"} is {a: string} which is also satisfied by {a: undefined}. And since that implementation of Spread<L, R> treats any possibly-undefined property as optional, you get that behavior. It is possible to detect optional properties in a way that distinguishes them from required-but-possibly-undefined properties:
typeOptionalPropertyNames<T>={[KinkeyofT]-?:
({}extends{[PinK]: T[K]} ? K : never)}[keyofT]
which should fix that particular issue, but I feel unclean even thinking about what happens when --strictNullChecks is disabled. Even with --strictNullChecks enabled it's still not really possible to consistently distinguish missing from undefined in TypeScript, which makes it hard to even say what the "right" thing to do is. If your test suite contains assign({a: 1}, {a: undefined}), you are probably heading down a dark path and need to turn back before it is too late.
... and that's my call to go to bed (don't look at my time zone 😉).
If your test suite contains assign({a: 1}, {a: undefined}), you are probably heading down a dark path and need to turn back before it is too late.
Looks like I'm too far gone. 😉
However, in all seriousness, Anders Hejlsberg (@ahejlsberg)'s implementation is fantastic and ticks all the boxes, my sincerest apologies for falsely calling out failing tests!
I'm not sure whether this issue should remain open to track the state of Object.assign, however if the TypeScript team wish to, I'd have little objections to this being closed.
There's an additional trade-off here because it's desirable for Object.assign(T, U) for two type parameters T and U to be assignable to both T and U (this comes up a lot in various code examples we've gotten reports of) - but in reality this isn't actually knowable unless you can prove that T and U will be instantiated with compatible-or-disjoint properties. Same for Object.assign(e, T); it is generally agreed that this should be assignable to typeof e in the absence of a concrete type for T. In contrast, the userland Spread<T, U> will not be assignable to T or U, nor would a hypothetical built-in type spread operator.
So anyway for concrete types, Spread is probably better, but for generics (or when at least one operand is generic) T & U is probably better, but there's no facility for overloading on generics.
it's desirable for Object.assign(T, U) for two type parameters T and U to be assignable to both T and U
That seems like a very peculiar thing to infer. I can understand why people may be expecting Object.assign(T, ...) to be assignable to T, but making assumptions about the source parameters seems particularly odd (they're almost always partials). Granted I don't at all doubt you when you say you've run into it.
EDIT: In the paragraph above "partials" was a very poor choice of word, as Tis indeed assignable to Partial<T>. What I really meant was "part of", or rather a type that is explicitly an incomplete subset of another type.
e.g. A React component might spread/assign its props (U) to a child component's props (T & U). In this case child's component's props (T & U) aren't necessarily (or even likely) to be assignable to the parent component's props (U).
Given that TypeScript wants to facilitate writing correct & maintainable software, I would ideally like to see changes that cause developers to disperse with these sorts of false assumptions. However, I totally understand what's proposed is a rather large change; and I'm certainly not expecting anyone to rashly make decisions. Nonetheless, I'm a bit sad to see this labelled as 'Working as intended' given the design goals of the language.
Anyway, I'll just keep my fingers crossed there'll be a change of heart at some point 🤞
Activity
jcalz commentedon Jun 19, 2019
Relevant comment (in #28553) saying why this is not already being done:
Relevant possible implementation (in #21316) of a
Spread<L, R>which is similar toAssign<A, B>. I don't know if anyone has a demonstrably superior implementation of this thing given all the potential edge cases like unknown, optional, and non-own properties:Link to code
Benjamin-Dobell commentedon Jun 19, 2019
Thanks, Joe Calzaretta (@jcalz), and great example!
It's bit of a shame as there's been a heap of improvements in soundness recently, really loving the direction!
Not that I think it's particularly compelling on its lonesome, but just for reference, I am seeing overlapping types being assigned. Specifically custom "sub-classes", default props of
number[]being overwritten withstring[],Date[]etc. Although, in the aforementioned case it's actually with a custom recursive assign in a JS library (chartist). I was improving the DefinitelyTyped definitions for chartist when I noticedObject.assignseemingly wasn't up to scratch.Anyway, I do totally understand the decision though. I guess it'd only be feasible with first class support for "exact types" (#12936), and my head is hurting just thinking about how much that'd further complicate the types here.
I also noted that when chained my
Assignisn't propagating the indexer; technically it needs to create a new indexer with a union of the return types in cases of:Hopefully this will become a bit more feasible in near future; I'm a sucker for soundness.
Benjamin-Dobell commentedon Jun 19, 2019
For future reference, whilst without a doubt it's far superior to my own attempt, Anders Hejlsberg (@ahejlsberg)'s implementation unfortunately does have a couple of failures when run against my test suite. Failing
test1in particular seems non-ideal. However, just to be clear, Anders Hejlsberg (@ahejlsberg) does a much better job of dealing with prickly edge cases (i.e. those mentioned above).EDIT: It's quite conceivable that I can't just naively apply the
Spreadtype to myassign, and that's responsible for 2 failed tests.jcalz commentedon Jun 19, 2019
You've got
--strictNullChecksdisabled there (well, the Playground does that by default), and therefore the inferred type of{a: "one"}is{a: string}which is also satisfied by{a: undefined}. And since that implementation ofSpread<L, R>treats any possibly-undefined property as optional, you get that behavior. It is possible to detect optional properties in a way that distinguishes them from required-but-possibly-undefined properties:which should fix that particular issue, but I feel unclean even thinking about what happens when
--strictNullChecksis disabled. Even with--strictNullChecksenabled it's still not really possible to consistently distinguish missing fromundefinedin TypeScript, which makes it hard to even say what the "right" thing to do is. If your test suite containsassign({a: 1}, {a: undefined}), you are probably heading down a dark path and need to turn back before it is too late.Benjamin-Dobell commentedon Jun 19, 2019
Ugh, spot on, thank you! 😊
... and that's my call to go to bed (don't look at my time zone 😉).
Looks like I'm too far gone. 😉
However, in all seriousness, Anders Hejlsberg (@ahejlsberg)'s implementation is fantastic and ticks all the boxes, my sincerest apologies for falsely calling out failing tests!
I'm not sure whether this issue should remain open to track the state of
Object.assign, however if the TypeScript team wish to, I'd have little objections to this being closed.RyanCavanaugh commentedon Jun 26, 2019
There's an additional trade-off here because it's desirable for
Object.assign(T, U)for two type parametersTandUto be assignable to bothTandU(this comes up a lot in various code examples we've gotten reports of) - but in reality this isn't actually knowable unless you can prove thatTandUwill be instantiated with compatible-or-disjoint properties. Same forObject.assign(e, T); it is generally agreed that this should be assignable totypeof ein the absence of a concrete type forT. In contrast, the userlandSpread<T, U>will not be assignable toTorU, nor would a hypothetical built-in type spread operator.So anyway for concrete types,
Spreadis probably better, but for generics (or when at least one operand is generic)T & Uis probably better, but there's no facility for overloading on generics.Benjamin-Dobell commentedon Jun 26, 2019
That seems like a very peculiar thing to infer. I can understand why people may be expecting
Object.assign(T, ...)to be assignable toT, but making assumptions about the source parameters seems particularly odd (they're almost always partials). Granted I don't at all doubt you when you say you've run into it.EDIT: In the paragraph above "partials" was a very poor choice of word, as
Tis indeed assignable toPartial<T>. What I really meant was "part of", or rather a type that is explicitly an incomplete subset of another type.e.g. A React component might spread/assign its props (
U) to a child component's props (T & U). In this case child's component's props (T & U) aren't necessarily (or even likely) to be assignable to the parent component's props (U).Given that TypeScript wants to facilitate writing correct & maintainable software, I would ideally like to see changes that cause developers to disperse with these sorts of false assumptions. However, I totally understand what's proposed is a rather large change; and I'm certainly not expecting anyone to rashly make decisions. Nonetheless, I'm a bit sad to see this labelled as 'Working as intended' given the design goals of the language.
Anyway, I'll just keep my fingers crossed there'll be a change of heart at some point 🤞
RyanCavanaugh commentedon Jun 26, 2019
Maybe "Best Available Compromise" is the better label
typescript-bot commentedon Jun 28, 2019
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.