Open justingrant opened 3 years ago
Apparently (see https://github.com/microsoft/TypeScript/issues/37182) the root cause is that TS doesn't include well-known symbols in keyof
results. So any transformation of object types using keyof
will lose symbols and cause this bug.
I tried to see if this could be fixed by patching types-external.ts. The changes below fixed the problem in my code:
/**
* Symbols are not (in TS 4.1) included in [K in keyof T] expressions. The type
* below adds symbols back.
*/
type WellKnownSymbols<T> = (T extends {[Symbol.asyncIterator]: infer V}
? {[Symbol.asyncIterator]: V}
: {}) &
(T extends {[Symbol.hasInstance]: infer V} ? {[Symbol.hasInstance]: V} : {}) &
(T extends {[Symbol.isConcatSpreadable]: infer V}
? {[Symbol.isConcatSpreadable]: V}
: {}) &
(T extends {[Symbol.iterator]: infer V} ? {[Symbol.iterator]: V} : {}) &
(T extends {[Symbol.match]: infer V} ? {[Symbol.match]: V} : {}) &
// @ts-ignore matchAll may be used in later lib versions
(T extends {[Symbol.matchAll]: infer V} ? {[Symbol.matchAll]: V} : {}) &
(T extends {[Symbol.replace]: infer V} ? {[Symbol.replace]: V} : {}) &
(T extends {[Symbol.search]: infer V} ? {[Symbol.search]: V} : {}) &
(T extends {[Symbol.split]: infer V} ? {[Symbol.split]: V} : {}) &
(T extends {[Symbol.species]: infer V} ? {[Symbol.species]: V} : {}) &
(T extends {[Symbol.toPrimitive]: infer V} ? {[Symbol.toPrimitive]: V} : {}) &
(T extends {[Symbol.toStringTag]: infer V} ? {[Symbol.toStringTag]: V} : {}) &
(T extends {[Symbol.unscopables]: infer V} ? {[Symbol.unscopables]: V} : {})
export type WritableDraft<T> = {-readonly [K in keyof T]: Draft<T[K]>} &
WellKnownSymbols<T>
Unfortunately, the changes above broke a few tests because spec.ts doesn't consider A & B
to be equivalent to A
. So I tried to patch spec.ts with similar changes:
/**
* Symbols are not (as of TS 4.1) included in [K in keyof T] expressions. The
* type below adds symbols back. Remove this if
* https://github.com/microsoft/TypeScript/pull/24738 lands. See also:
* https://github.com/microsoft/TypeScript/issues/24622 and
* https://github.com/microsoft/TypeScript/issues/37182#issuecomment-594933808
*/
type WellKnownSymbols<T> = (T extends { [Symbol.asyncIterator]: infer V }
? // @ts-ignore asyncIterator requires later lib version than Symbol
{ [Symbol.asyncIterator]: V }
: {}) &
(T extends { [Symbol.hasInstance]: infer V } ? { [Symbol.hasInstance]: V } : {}) &
(T extends { [Symbol.isConcatSpreadable]: infer V }
? { [Symbol.isConcatSpreadable]: V }
: {}) &
(T extends { [Symbol.iterator]: infer V } ? { [Symbol.iterator]: V } : {}) &
(T extends { [Symbol.match]: infer V } ? { [Symbol.match]: V } : {}) &
// @ts-ignore matchAll requires later lib version than Symbol
(T extends { [Symbol.matchAll]: infer V } ? { [Symbol.matchAll]: V } : {}) &
(T extends { [Symbol.replace]: infer V } ? { [Symbol.replace]: V } : {}) &
(T extends { [Symbol.search]: infer V } ? { [Symbol.search]: V } : {}) &
(T extends { [Symbol.split]: infer V } ? { [Symbol.split]: V } : {}) &
(T extends { [Symbol.species]: infer V } ? { [Symbol.species]: V } : {}) &
(T extends { [Symbol.toPrimitive]: infer V } ? { [Symbol.toPrimitive]: V } : {}) &
(T extends { [Symbol.toStringTag]: infer V } ? { [Symbol.toStringTag]: V } : {}) &
(T extends { [Symbol.unscopables]: infer V } ? { [Symbol.unscopables]: V } : {});
type WithSymbols<T> = T extends object ? T & WellKnownSymbols<T> : T
// prettier-ignore
export type MergeInsertions<T> = T extends object
? { [K in keyof WithSymbols<T>]: MergeInsertions<WithSymbols<T>[K]> }
: T;
export type TestAlike<X, Y> = TestExact<MergeInsertions<X>, MergeInsertions<Y>>;
export type Test<Left, Right> = IsAny<Left> extends 1
? IsAny<Right> extends 1
? 1
: "❌ Left type is 'any' but right type is not"
: IsAny<Right> extends 1
? "❌ Right type is 'any' but left type is not"
: [Left] extends [Right]
? [Right] extends [Left]
? Any extends TestAlike<Left, Right> // replaced TestExact with TestAlike
? 1
: "❌ Unexpected or missing 'readonly' property"
: "❌ Right type is not assignable to left type"
: "❌ Left type is not assignable to right type";
These spec.ts changes broke Immer tests that used readonly arrays, probably because TS doesn't have a concept of "read-only iterators" so the iterator coming from a readonly array was a writable iterator.
At this point, I'm stumped! It's possible there may not be a perfect solution until TS lands https://github.com/microsoft/TypeScript/pull/24738, although my experiments above suggest that there may be least-bad solutions possible with some tradeoffs.
Any ideas how to fix this without the tradeoffs noted above?
Did you try the cast utilities? https://immerjs.github.io/immer/typescript#cast-utilities
Did you try the cast utilities? https://immerjs.github.io/immer/typescript#cast-utilities
This doesn't work with those as in the original example above, Draft<Foo>[Symbol.toStringTag]
is never
. Because Draft
and Immutable
drop them.
🐛 Bug Report
Draft<T>
objects (at least according to TS) don't bring along any symbol-indexed properties from the original object. Is this expected? I'm not sure if this is only an issue in the TS types, or is the underlying JS draft object also missing its symbol-indexed properties?Although well-known symbols are usually used with classes, I was able to repro the problem with a plain object too.
I'm using TS 4.1, if it matters.
FWIW, I ran into this because I was trying to call
produce()
with a deeply-nested object which had aUint8Array
-valued property far down in the tree. When I tried to pass thedraft
object to another method that expected the original type, TS complained that[Symbol.iterator]
and[Symbol.toStringTag]
properties that were required onUint8Array
but missing on theDraft<T>
object. I'm not trying to mutate the array (that'd be #696); I'm just expecting a deep clone but (at least from TS's perspective) the symbol properties are missing.Link to repro
https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBGKEAmBXAxgUwDRwN5wAiUAhgGbwC+cZSIcA5KCJlAwNwBQn6EAdgGd4ZCBDgBefJzhwSACgCU+OFEwxUUPnAAsAJnZxK2aXADmauAG0AygE8QAIwgAbAHQwI1mFGB9TAFRJTAF1FZVV1TUYRCA5DTkpuGFswTDgAMVEJOGTUiDIaUS5OMlQ+dBhgfjgAd2AYAAtMiDkyAC4M0SU8EwiNLTIbeyc3Dy8fP0CQrkSefiE4CABrbJihxxd3T29fAKDggzgAeiO4PjFWJCg5wXhl3Wy6xubWrsOTs4uoK+5eW4QkGgsFBsogUBhMHI5MhSBQOsRyDAADzNAB8SnEqKkMhhiNc8gU71O5zgl2gJieTVE0NhMEJJiOACoZCy4AA9DkckwyACCUFMqBYfDuBVyaQYAHUfDASA5nJgERQkQQCR0+IKHKwDKoSMh+M5bFY7BtRtsJnsQh0hOaDJRUQw4Ny4MABJ94CQBAJgKY+LL5TkxGASKQWDBWItRSlxSrFGqNVqVJhdfrDesRltxrspsErTs-LaGK4nQAFJCpWCGhhpzZjPMW4IOl1wEAur1+Z1aMWMKX1P0K2nK2Sxs7xqAGJ06vV8A1G4Y1s1Z-a5m2Ge1wByoeCqACOqGAqmQHZyUcdrIYMYUcccCcnKdnJozdezy92BdccDkugAzNoAKwKExGSObgZD+BYyBIYBnGyXEKGrU1M0mfYuBkJlWXQmROSw7CcNwp04AAUXlIV4FAMBnGAdB6hnBoPVkLQGBIPhbAdLtNXQEhUAENJMAAD0QTBPSqLR8mPVJGAEOdnAdDi+AYeBNTgLjMGQfCPA7ZA+LE8UexlOV+0RQdVRHa8x0TZNp1TY101rc1nzga1X1XQs5AAdgABl-L8AJkICEnpIA
LMK if you want me to turn the TS playground code above into a CodeSandbox example; happy to do so.
To Reproduce
Steps to reproduce the behavior: 1) Create a producer that accepts a TS type that has a well-known symbol property 2) Try to read that property or call a function that accepts a parameter of
T
fromDraft<T>
. Note that mutation is not required-- this problem affects read-only access.Observed behavior
TS errors as noted above.
Expected behavior
No TS errors.
Environment
We only accept bug reports against the latest Immer version.
setUseProxies(true)
setUseProxies(false)
(ES5 only)