From d4abbd876d3b7b364082ff334614ae2c04ec61ad Mon Sep 17 00:00:00 2001 From: markostanimirovic Date: Tue, 3 Dec 2024 00:19:29 +0100 Subject: [PATCH 1/2] fix(signals): create deep signals for custom class instances --- modules/signals/spec/deep-signal.spec.ts | 113 ++++++++++++++++++ .../spec/types/signal-state.types.spec.ts | 62 +++++++++- .../spec/types/signal-store.types.spec.ts | 52 +++++++- modules/signals/src/deep-signal.ts | 22 +++- modules/signals/src/ts-helpers.ts | 15 +-- 5 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 modules/signals/spec/deep-signal.spec.ts diff --git a/modules/signals/spec/deep-signal.spec.ts b/modules/signals/spec/deep-signal.spec.ts new file mode 100644 index 0000000000..0a0e1726d9 --- /dev/null +++ b/modules/signals/spec/deep-signal.spec.ts @@ -0,0 +1,113 @@ +import { toDeepSignal } from '../src/deep-signal'; +import { isSignal, signal } from '@angular/core'; + +describe('toDeepSignal', () => { + it('creates deep signals for plain objects', () => { + const sig = signal({ m: { s: 't' } }); + const deepSig = toDeepSignal(sig); + + expect(sig).not.toBe(deepSig); + + expect(isSignal(deepSig)).toBe(true); + expect(deepSig()).toEqual({ m: { s: 't' } }); + + expect(isSignal(deepSig.m)).toBe(true); + expect(deepSig.m()).toEqual({ s: 't' }); + + expect(isSignal(deepSig.m.s)).toBe(true); + expect(deepSig.m.s()).toBe('t'); + }); + + it('creates deep signals for custom class instances', () => { + class User { + constructor(readonly firstName: string) {} + } + + class UserState { + constructor(readonly user: User) {} + } + + const sig = signal(new UserState(new User('John'))); + const deepSig = toDeepSignal(sig); + + expect(sig).not.toBe(deepSig); + + expect(isSignal(deepSig)).toBe(true); + expect(deepSig()).toEqual({ user: { firstName: 'John' } }); + + expect(isSignal(deepSig.user)).toBe(true); + expect(deepSig.user()).toEqual({ firstName: 'John' }); + + expect(isSignal(deepSig.user.firstName)).toBe(true); + expect(deepSig.user.firstName()).toBe('John'); + }); + + it('does not create deep signals for primitives', () => { + const num = signal(0); + const str = signal('str'); + const bool = signal(true); + + const deepNum = toDeepSignal(num); + const deepStr = toDeepSignal(str); + const deepBool = toDeepSignal(bool); + + expect(deepNum).toBe(num); + expect(deepStr).toBe(str); + expect(deepBool).toBe(bool); + }); + + it('does not create deep signals for built-in object types', () => { + 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 deepArray = toDeepSignal(array); + const deepSet = toDeepSignal(set); + const deepMap = toDeepSignal(map); + const deepDate = toDeepSignal(date); + const deepError = toDeepSignal(error); + const deepRegExp = toDeepSignal(regExp); + + expect(deepArray).toBe(array); + expect(deepSet).toBe(set); + expect(deepMap).toBe(map); + expect(deepDate).toBe(date); + expect(deepError).toBe(error); + expect(deepRegExp).toBe(regExp); + }); + + it('does not create deep signals for functions', () => { + const fn1 = signal(new Function()); + const fn2 = signal(function () {}); + const fn3 = signal(() => {}); + + const deepFn1 = toDeepSignal(fn1); + const deepFn2 = toDeepSignal(fn2); + const deepFn3 = toDeepSignal(fn3); + + expect(deepFn1).toBe(fn1); + expect(deepFn2).toBe(fn2); + expect(deepFn3).toBe(fn3); + }); + + it('does not create deep signals for custom class instances that extend built-in object types', () => { + class CustomArray extends Array {} + class CustomSet extends Set {} + class CustomError extends Error {} + + const array = signal(new CustomArray()); + const set = signal(new CustomSet()); + const error = signal(new CustomError()); + + const deepArray = toDeepSignal(array); + const deepSet = toDeepSignal(set); + const deepError = toDeepSignal(error); + + expect(deepArray).toBe(array); + expect(deepSet).toBe(set); + expect(deepError).toBe(error); + }); +}); diff --git a/modules/signals/spec/types/signal-state.types.spec.ts b/modules/signals/spec/types/signal-state.types.spec.ts index c9edeab141..d7fe53c47a 100644 --- a/modules/signals/spec/types/signal-state.types.spec.ts +++ b/modules/signals/spec/types/signal-state.types.spec.ts @@ -132,6 +132,20 @@ describe('signalState', () => { ); }); + 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', + 'unique symbol | keyof Signal>' + ); + }); + it('does not create deep signals for Map', () => { const snippet = ` const state = signalState(new Map()); @@ -146,9 +160,9 @@ describe('signalState', () => { ); }); - it('does not create deep signals for Set', () => { + it('does not create deep signals for Date', () => { const snippet = ` - const state = signalState(new Set()); + const state = signalState(new Date()); declare const stateKeys: keyof typeof state; `; @@ -156,7 +170,49 @@ describe('signalState', () => { expectSnippet(snippet).toInfer( 'stateKeys', - 'unique symbol | keyof Signal>' + 'unique symbol | keyof Signal' + ); + }); + + it('does not create deep signals for Error', () => { + const snippet = ` + const state = signalState(new Error()); + declare const stateKeys: keyof typeof state; + `; + + expectSnippet(snippet).toSucceed(); + + expectSnippet(snippet).toInfer( + 'stateKeys', + '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).toSucceed(); + + expectSnippet(snippet).toInfer( + 'stateKeys', + 'unique symbol | keyof Signal' + ); + }); + + it('does not create deep signals for Function', () => { + const snippet = ` + const state = signalState(() => {}); + declare const stateKeys: keyof typeof state; + `; + + expectSnippet(snippet).toSucceed(); + + expectSnippet(snippet).toInfer( + 'stateKeys', + 'unique symbol | keyof Signal<() => void>' ); }); diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index aeb0ab73f4..85f2f5f5a2 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -175,6 +175,18 @@ describe('signalStore', () => { 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())); @@ -187,9 +199,45 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer('storeKeys', 'unique symbol'); }); - it('does not create deep signals when state type is Set', () => { + it('does not create deep signals when state type is Date', () => { const snippet = ` - const Store = signalStore(withState(new Set<{ foo: string }>())); + const Store = signalStore(withState(new Date())); + 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 Error', () => { + const snippet = ` + const Store = signalStore(withState(new Error())); + 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 RegExp', () => { + const snippet = ` + const Store = signalStore(withState(new RegExp(''))); + 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 Function', () => { + const snippet = ` + const Store = signalStore(withState(() => () => {})); const store = new Store(); declare const storeKeys: keyof typeof store; `; diff --git a/modules/signals/src/deep-signal.ts b/modules/signals/src/deep-signal.ts index 733e085fc7..2342fd811a 100644 --- a/modules/signals/src/deep-signal.ts +++ b/modules/signals/src/deep-signal.ts @@ -47,5 +47,25 @@ export function toDeepSignal(signal: Signal): DeepSignal { } function isRecord(value: unknown): value is Record { - return value?.constructor === Object; + if (value === null || typeof value !== 'object') { + return false; + } + + let proto = Object.getPrototypeOf(value); + if (proto === Object.prototype) { + return true; + } + + while (proto && proto !== Object.prototype) { + if ( + [Array, Set, Map, Date, Error, RegExp, Function].includes( + proto.constructor + ) + ) { + return false; + } + proto = Object.getPrototypeOf(proto); + } + + return proto === Object.prototype; } diff --git a/modules/signals/src/ts-helpers.ts b/modules/signals/src/ts-helpers.ts index f7df5cdb77..4117c1502e 100644 --- a/modules/signals/src/ts-helpers.ts +++ b/modules/signals/src/ts-helpers.ts @@ -1,13 +1,14 @@ export type Prettify = { [K in keyof T]: T[K] } & {}; export type IsRecord = T extends object - ? T extends unknown[] - ? false - : T extends Set - ? false - : T extends Map - ? false - : T extends Function + ? T extends + | unknown[] + | Set + | Map + | Date + | Error + | RegExp + | Function ? false : true : false; From 5d66296819f2416b7abde0c4ff9a2852639686b2 Mon Sep 17 00:00:00 2001 From: markostanimirovic Date: Mon, 9 Dec 2024 22:26:00 +0100 Subject: [PATCH 2/2] fix: extend check to other built-in object types --- modules/signals/spec/deep-signal.spec.ts | 72 ++++++++++++--- .../spec/types/signal-state.types.spec.ts | 90 +++++++++---------- .../spec/types/signal-store.types.spec.ts | 88 ++++++++---------- modules/signals/src/deep-signal.ts | 24 +++-- modules/signals/src/ts-helpers.ts | 21 +++-- 5 files changed, 167 insertions(+), 128 deletions(-) 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;