diff --git a/modules/signals/spec/deep-signal.spec.ts b/modules/signals/spec/deep-signal.spec.ts index 0a0e1726d9..0fdd89795e 100644 --- a/modules/signals/spec/deep-signal.spec.ts +++ b/modules/signals/spec/deep-signal.spec.ts @@ -1,5 +1,5 @@ -import { toDeepSignal } from '../src/deep-signal'; import { isSignal, signal } from '@angular/core'; +import { toDeepSignal } from '../src/deep-signal'; describe('toDeepSignal', () => { it('creates deep signals for plain objects', () => { @@ -56,27 +56,53 @@ describe('toDeepSignal', () => { expect(deepBool).toBe(bool); }); - it('does not create deep signals for built-in object types', () => { + it('does not create deep signals for iterables', () => { const array = signal([]); const set = signal(new Set()); const map = signal(new Map()); - const date = signal(new Date()); - const error = signal(new Error()); - const regExp = signal(new RegExp('')); + const uintArray = signal(new Uint32Array()); + const floatArray = signal(new Float64Array()); const deepArray = toDeepSignal(array); const deepSet = toDeepSignal(set); const deepMap = toDeepSignal(map); - const deepDate = toDeepSignal(date); - const deepError = toDeepSignal(error); - const deepRegExp = toDeepSignal(regExp); + const deepUintArray = toDeepSignal(uintArray); + const deepFloatArray = toDeepSignal(floatArray); expect(deepArray).toBe(array); expect(deepSet).toBe(set); expect(deepMap).toBe(map); + expect(deepUintArray).toBe(uintArray); + expect(deepFloatArray).toBe(floatArray); + }); + + it('does not create deep signals for built-in object types', () => { + const weakSet = signal(new WeakSet()); + const weakMap = signal(new WeakMap()); + const promise = signal(Promise.resolve(10)); + const date = signal(new Date()); + const error = signal(new Error()); + const regExp = signal(new RegExp('')); + const arrayBuffer = signal(new ArrayBuffer(10)); + const dataView = signal(new DataView(new ArrayBuffer(10))); + + const deepWeakSet = toDeepSignal(weakSet); + const deepWeakMap = toDeepSignal(weakMap); + const deepPromise = toDeepSignal(promise); + const deepDate = toDeepSignal(date); + const deepError = toDeepSignal(error); + const deepRegExp = toDeepSignal(regExp); + const deepArrayBuffer = toDeepSignal(arrayBuffer); + const deepDataView = toDeepSignal(dataView); + + expect(deepWeakSet).toBe(weakSet); + expect(deepWeakMap).toBe(weakMap); + expect(deepPromise).toBe(promise); expect(deepDate).toBe(date); expect(deepError).toBe(error); expect(deepRegExp).toBe(regExp); + expect(deepArrayBuffer).toBe(arrayBuffer); + expect(deepDataView).toBe(dataView); }); it('does not create deep signals for functions', () => { @@ -93,21 +119,43 @@ describe('toDeepSignal', () => { expect(deepFn3).toBe(fn3); }); - it('does not create deep signals for custom class instances that extend built-in object types', () => { + it('does not create deep signals for custom class instances that are iterables', () => { class CustomArray extends Array {} + class CustomSet extends Set {} - class CustomError extends Error {} + + class CustomFloatArray extends Float32Array {} const array = signal(new CustomArray()); + const floatArray = signal(new CustomFloatArray()); const set = signal(new CustomSet()); - const error = signal(new CustomError()); const deepArray = toDeepSignal(array); + const deepFloatArray = toDeepSignal(floatArray); const deepSet = toDeepSignal(set); - const deepError = toDeepSignal(error); expect(deepArray).toBe(array); + expect(deepFloatArray).toBe(floatArray); expect(deepSet).toBe(set); + }); + + it('does not create deep signals for custom class instances that extend built-in object types', () => { + class CustomWeakMap extends WeakMap {} + + class CustomError extends Error {} + + class CustomArrayBuffer extends ArrayBuffer {} + + const weakMap = signal(new CustomWeakMap()); + const error = signal(new CustomError()); + const arrayBuffer = signal(new CustomArrayBuffer(10)); + + const deepWeakMap = toDeepSignal(weakMap); + const deepError = toDeepSignal(error); + const deepArrayBuffer = toDeepSignal(arrayBuffer); + + expect(deepWeakMap).toBe(weakMap); expect(deepError).toBe(error); + expect(deepArrayBuffer).toBe(arrayBuffer); }); }); diff --git a/modules/signals/spec/types/signal-state.types.spec.ts b/modules/signals/spec/types/signal-state.types.spec.ts index d7fe53c47a..e4bf63b2ad 100644 --- a/modules/signals/spec/types/signal-state.types.spec.ts +++ b/modules/signals/spec/types/signal-state.types.spec.ts @@ -118,91 +118,83 @@ describe('signalState', () => { expectSnippet(snippet).toInfer('set', 'Signal>'); }); - it('does not create deep signals for an array', () => { + it('does not create deep signals for iterables', () => { const snippet = ` - const state = signalState([]); - declare const stateKeys: keyof typeof state; + const arrayState = signalState([]); + declare const arrayStateKeys: keyof typeof arrayState; + + const setState = signalState(new Set()); + declare const setStateKeys: keyof typeof setState; + + const mapState = signalState(new Map()); + declare const mapStateKeys: keyof typeof mapState; + + const uintArrayState = signalState(new Uint8ClampedArray()); + declare const uintArrayStateKeys: keyof typeof uintArrayState; `; expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', + 'arrayStateKeys', 'unique symbol | keyof Signal' ); - }); - - it('does not create deep signals for Set', () => { - const snippet = ` - const state = signalState(new Set()); - declare const stateKeys: keyof typeof state; - `; - - expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', + 'setStateKeys', 'unique symbol | keyof Signal>' ); - }); - - it('does not create deep signals for Map', () => { - const snippet = ` - const state = signalState(new Map()); - declare const stateKeys: keyof typeof state; - `; - - expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', + 'mapStateKeys', 'unique symbol | keyof Signal>' ); - }); - - it('does not create deep signals for Date', () => { - const snippet = ` - const state = signalState(new Date()); - declare const stateKeys: keyof typeof state; - `; - - expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', - 'unique symbol | keyof Signal' + 'uintArrayStateKeys', + 'unique symbol | keyof Signal' ); }); - it('does not create deep signals for Error', () => { + it('does not create deep signals for built-in object types', () => { const snippet = ` - const state = signalState(new Error()); - declare const stateKeys: keyof typeof state; + const weakSetState = signalState(new WeakSet<{ foo: string }>()); + declare const weakSetStateKeys: keyof typeof weakSetState; + + const dateState = signalState(new Date()); + declare const dateStateKeys: keyof typeof dateState; + + const errorState = signalState(new Error()); + declare const errorStateKeys: keyof typeof errorState; + + const regExpState = signalState(new RegExp('')); + declare const regExpStateKeys: keyof typeof regExpState; `; expectSnippet(snippet).toSucceed(); expectSnippet(snippet).toInfer( - 'stateKeys', - 'unique symbol | keyof Signal' + 'weakSetStateKeys', + 'unique symbol | keyof Signal>' ); - }); - it('does not create deep signals for RegExp', () => { - const snippet = ` - const state = signalState(new RegExp('')); - declare const stateKeys: keyof typeof state; - `; + expectSnippet(snippet).toInfer( + 'dateStateKeys', + 'unique symbol | keyof Signal' + ); - expectSnippet(snippet).toSucceed(); + expectSnippet(snippet).toInfer( + 'errorStateKeys', + 'unique symbol | keyof Signal' + ); expectSnippet(snippet).toInfer( - 'stateKeys', + 'regExpStateKeys', 'unique symbol | keyof Signal' ); }); - it('does not create deep signals for Function', () => { + it('does not create deep signals for functions', () => { const snippet = ` const state = signalState(() => {}); declare const stateKeys: keyof typeof state; diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index 85f2f5f5a2..6854f27064 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -163,79 +163,61 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer('set', 'Signal>'); }); - it('does not create deep signals when state type is an array', () => { + it('does not create deep signals when state type is an iterable', () => { const snippet = ` - const Store = signalStore(withState([])); - const store = new Store(); - declare const storeKeys: keyof typeof store; - `; - - expectSnippet(snippet).toSucceed(); - - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); - }); - - it('does not create deep signals when state type is Set', () => { - const snippet = ` - const Store = signalStore(withState(new Set<{ foo: string }>())); - const store = new Store(); - declare const storeKeys: keyof typeof store; - `; - - expectSnippet(snippet).toSucceed(); - - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); - }); - - it('does not create deep signals when state type is Map', () => { - const snippet = ` - const Store = signalStore(withState(new Map())); - const store = new Store(); - declare const storeKeys: keyof typeof store; - `; + const ArrayStore = signalStore(withState([])); + const arrayStore = new ArrayStore(); + declare const arrayStoreKeys: keyof typeof arrayStore; - expectSnippet(snippet).toSucceed(); + const SetStore = signalStore(withState(new Set<{ foo: string }>())); + const setStore = new SetStore(); + declare const setStoreKeys: keyof typeof setStore; - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); - }); + const MapStore = signalStore(withState(new Map())); + const mapStore = new MapStore(); + declare const mapStoreKeys: keyof typeof mapStore; - it('does not create deep signals when state type is Date', () => { - const snippet = ` - const Store = signalStore(withState(new Date())); - const store = new Store(); - declare const storeKeys: keyof typeof store; + const FloatArrayStore = signalStore(withState(new Float32Array())); + const floatArrayStore = new FloatArrayStore(); + declare const floatArrayStoreKeys: keyof typeof floatArrayStore; `; expectSnippet(snippet).toSucceed(); - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('arrayStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('setStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('mapStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('floatArrayStoreKeys', 'unique symbol'); }); - it('does not create deep signals when state type is Error', () => { + it('does not create deep signals when state type is a built-in object type', () => { const snippet = ` - const Store = signalStore(withState(new Error())); - const store = new Store(); - declare const storeKeys: keyof typeof store; - `; + const WeakMapStore = signalStore(withState(new WeakMap<{ foo: string }, { bar: number }>())); + const weakMapStore = new WeakMapStore(); + declare const weakMapStoreKeys: keyof typeof weakMapStore; - expectSnippet(snippet).toSucceed(); + const DateStore = signalStore(withState(new Date())); + const dateStore = new DateStore(); + declare const dateStoreKeys: keyof typeof dateStore; - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); - }); + const ErrorStore = signalStore(withState(new Error())); + const errorStore = new ErrorStore(); + declare const errorStoreKeys: keyof typeof errorStore; - it('does not create deep signals when state type is RegExp', () => { - const snippet = ` - const Store = signalStore(withState(new RegExp(''))); - const store = new Store(); - declare const storeKeys: keyof typeof store; + const RegExpStore = signalStore(withState(new RegExp(''))); + const regExpStore = new RegExpStore(); + declare const regExpStoreKeys: keyof typeof regExpStore; `; expectSnippet(snippet).toSucceed(); - expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('weakMapStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('dateStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('errorStoreKeys', 'unique symbol'); + expectSnippet(snippet).toInfer('regExpStoreKeys', 'unique symbol'); }); - it('does not create deep signals when state type is Function', () => { + it('does not create deep signals when state type is a function', () => { const snippet = ` const Store = signalStore(withState(() => () => {})); const store = new Store(); diff --git a/modules/signals/src/deep-signal.ts b/modules/signals/src/deep-signal.ts index 2342fd811a..7610d9ad8e 100644 --- a/modules/signals/src/deep-signal.ts +++ b/modules/signals/src/deep-signal.ts @@ -46,8 +46,20 @@ export function toDeepSignal(signal: Signal): DeepSignal { }); } +const nonRecords = [ + WeakSet, + WeakMap, + Promise, + Date, + Error, + RegExp, + ArrayBuffer, + DataView, + Function, +]; + function isRecord(value: unknown): value is Record { - if (value === null || typeof value !== 'object') { + if (value === null || typeof value !== 'object' || isIterable(value)) { return false; } @@ -57,11 +69,7 @@ function isRecord(value: unknown): value is Record { } while (proto && proto !== Object.prototype) { - if ( - [Array, Set, Map, Date, Error, RegExp, Function].includes( - proto.constructor - ) - ) { + if (nonRecords.includes(proto.constructor)) { return false; } proto = Object.getPrototypeOf(proto); @@ -69,3 +77,7 @@ function isRecord(value: unknown): value is Record { return proto === Object.prototype; } + +function isIterable(value: any): value is Iterable { + return typeof value?.[Symbol.iterator] === 'function'; +} diff --git a/modules/signals/src/ts-helpers.ts b/modules/signals/src/ts-helpers.ts index 4117c1502e..bf94d5965b 100644 --- a/modules/signals/src/ts-helpers.ts +++ b/modules/signals/src/ts-helpers.ts @@ -1,14 +1,19 @@ +type NonRecord = + | Iterable + | WeakSet + | WeakMap + | Promise + | Date + | Error + | RegExp + | ArrayBuffer + | DataView + | Function; + export type Prettify = { [K in keyof T]: T[K] } & {}; export type IsRecord = T extends object - ? T extends - | unknown[] - | Set - | Map - | Date - | Error - | RegExp - | Function + ? T extends NonRecord ? false : true : false;