signalState is not creating DeepSignal property signals when the value object is an instance of a class
Closed this issue · 2 comments
Which @ngrx/* package(s) are the source of the bug?
signals
Minimal reproduction of the bug/regression with instructions
Using signalState, create a new store where the type of the value in the store is a class, and the value is set to an instance of that class. The documentation states that when the value in the store is an object, signals will be created for each property on the object (DeepSignal). But in the case where the object is a class instance, signals are not being created for each property, they are undefined.
If this isn't supported, that's one thing, but TypeScript still types each property on the value as a signal.
Minimal reproduction: https://stackblitz.com/edit/typescript-dxfawb?file=index.ts
Expected behavior
Signals are created for each property on the signalStore value when the value is an instance of a class.
Versions of NgRx, Angular, Node, affected browser(s) and operating system(s)
NgRx: 18+
Other information
No response
I would be willing to submit a PR to fix this issue
- Yes
- No
Hi @vtachkov, I haven’t found a solution yet. I’ve shared my PR with the experiments I’ve tried so far. If you or anyone else discovers a solution, your input would be greatly appreciated!
Here is the link to the TypeScript Playground which shows the current status. it is not working btw. Any help is highly appreciated.
For your convenience, here's the code:
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
export type IsRecord<T> = T extends object
? T extends unknown[]
? false
: T extends Set<unknown>
? false
: T extends Map<unknown, unknown>
? false
: T extends Function
? false
: T extends { prototype: unknown } // check for constructor did not work
? false
: T extends { [key: string]: unknown }
? true
: false
: false;
export type IsUnknownRecord<T> = string extends keyof T
? true
: number extends keyof T
? true
: false;
export type IsKnownRecord<T> = IsRecord<T> extends true
? IsUnknownRecord<T> extends true
? false
: true
: false;
export type OmitPrivate<T> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K];
};
type NgSignal<T> = {set(value: T): void, update: (updateFn: (value: T) => T) => void} & (() => T)
// An extended Signal type that enables the correct typing
// of nested signals with the `name` or `length` key.
export interface Signal<T> extends NgSignal<T> {
name: unknown;
length: unknown;
}
export type DeepSignal<T> = Signal<T> &
(IsKnownRecord<T> extends true
? Readonly<{
[K in keyof T]: IsKnownRecord<T[K]> extends true
? DeepSignal<T[K]>
: Signal<T[K]>;
}>
: unknown);
declare function signalState<T>(value: T): DeepSignal<T>;
class User {
id = 0;
name = 'Konrad';
}
const state = {
user: new User(),
id: 1,
address: { city: 'Wien', zip: '1040' },
};
type AssertFalse<T extends false> = T;
type AssertTrue<T extends true> = T;
type IsType<Actual, Expected> = Expected extends Actual ? Actual extends Expected ? true : false;
const store = signalState(state);
type T1 = AssertFalse<IsRecord<User>>;
type T2 = AssertFalse<IsRecord<typeof state.id>>;
type T3 = AssertTrue<IsRecord<typeof state.address>>;
type T4 = AssertTrue<IsType<typeof store.address, DeepSignal<{city: string, zip: string}>>;
type T5 = AssertTrue<IsType<typeof store.user, Signal<User>>;
type T6 = AssertFalse<IsType<typeof store.user, DeepSignal<User>>; // fails because it is also true