From 7e6327b6568ed06007fb3c30e2f9c7c1d381e554 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 20:32:53 +0200 Subject: [PATCH 01/14] add missing variants to Primitve type --- src/observableInterfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 1f9656cdd..2aa8deee4 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -330,7 +330,7 @@ export interface ObservableRoot { activate?: () => void; } -export type Primitive = boolean | string | number | Date; +export type Primitive = boolean | string | number | Date | undefined | null | symbol | bigint; export type NotPrimitive = T extends Primitive ? never : T; export type ObservableMap | WeakMap> = Omit & From a03179652c4a86ba86cbbe79d24bc7a9a0b46887 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 22:32:35 +0200 Subject: [PATCH 02/14] fix 'Observable' types with nullable values --- src/observableInterfaces.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 2aa8deee4..7beadc4e3 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -96,7 +96,7 @@ export interface ListenerParams { export type ListenerFn = (params: ListenerParams) => void; type PrimitiveKeys = Pick; -type NonPrimitiveKeys = Pick; +type StripPrimitiveProps = Pick; type Recurse = T[K] extends ObservableReadable ? T[K] @@ -124,14 +124,20 @@ type Recurse = T[K] extends ObservableReadable ? TRecurse : T[K]; -type ObservableFnsRecursiveUnsafe = { - [K in keyof T]-?: Recurse>>; +type ObservableFnsRecursiveUnsafe = { + [K in keyof T]-?: Recurse>; }; -type ObservableFnsRecursiveSafe = { - readonly [K in keyof T]-?: Recurse>>; + +type ObservableFnsRecursiveSafe = { + readonly [K in keyof T]-?: Recurse>; +}; + +type MakeNullable = { + [K in keyof T]: T[K] | Nullable; }; -type ObservableFnsRecursive = ObservableFnsRecursiveSafe> & - ObservableFnsRecursiveUnsafe>; + +type ObservableFnsRecursive = ObservableFnsRecursiveSafe, Nullable> & + ObservableFnsRecursiveUnsafe, Nullable>, Nullable>; type ObservableComputedFnsRecursive = { readonly [K in keyof T]-?: Recurse>>; @@ -340,7 +346,20 @@ export type ObservableSet | WeakSet> = Omit & Omit, 'size'> & { size: ObservablePrimitiveChild }; export type ObservableMapIfMap = T extends Map | WeakMap ? ObservableMap : never; export type ObservableArray = ObservableObjectFns & ObservableArrayOverride; -export type ObservableObject = ObservableFnsRecursive & ObservableObjectFns; +export type ObservableObject = ObservableFnsRecursive< + NonNullable, + PickNullableAndUndefined +> & + ObservableObjectFns; + +export type PickNullableAndUndefined = T extends null + ? T extends undefined + ? null | undefined + : null + : T extends undefined + ? undefined + : never; + export type ObservableChild = [T] extends [Primitive] ? ObservablePrimitiveChild : ObservableObject; export type ObservablePrimitiveChild = [T] extends [boolean] ? ObservablePrimitiveChildFns & ObservablePrimitiveBooleanFns From 492188e3e7898200e964f6be5fd6a07e3fd680e7 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 22:35:11 +0200 Subject: [PATCH 03/14] fix signatures of observable constructing functions --- src/observable.ts | 6 ++++++ src/react/useObservable.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/observable.ts b/src/observable.ts index b65621834..84532466b 100644 --- a/src/observable.ts +++ b/src/observable.ts @@ -10,6 +10,8 @@ import type { ObservableRoot, } from './observableInterfaces'; +function createObservable(): Observable; +function createObservable(value: T | Promise, makePrimitive?: true): Observable; function createObservable(value?: T | Promise, makePrimitive?: true): ObservablePrimitive; function createObservable( value?: T | Promise, @@ -49,10 +51,14 @@ function createObservable( return obs; } +export function observable(): Observable; +export function observable(value: T | Promise): Observable; export function observable(value?: T | Promise): Observable { return createObservable(value) as Observable; } +export function observablePrimitive(): ObservablePrimitive; +export function observablePrimitive(value: T | Promise): ObservablePrimitive; export function observablePrimitive(value?: T | Promise): ObservablePrimitive { return createObservable(value, /*makePrimitive*/ true); } diff --git a/src/react/useObservable.ts b/src/react/useObservable.ts index 29b5dcece..c54b89eab 100644 --- a/src/react/useObservable.ts +++ b/src/react/useObservable.ts @@ -8,6 +8,8 @@ import { useMemo } from 'react'; * * @see https://www.legendapp.com/dev/state/react/#useObservable */ +export function useObservable(): Observable; +export function useObservable(initialValue: T | (() => T) | (() => Promise)): Observable; export function useObservable(initialValue?: T | (() => T) | (() => Promise)): Observable { // Create the observable from the default value return useMemo( From 7892f077acac064089302a1cf053ad490db6db19 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 22:37:03 +0200 Subject: [PATCH 04/14] add type assertion tests (temporary?) --- package.json | 1 + tests/types.test.ts | 161 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/types.test.ts diff --git a/package.json b/package.json index 3a0577c9b..d49d5eb3f 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "expect-type": "^0.16.0", "fake-indexeddb": "^4.0.0", "jest": "^29.6.1", "jest-environment-jsdom": "^29.6.1", diff --git a/tests/types.test.ts b/tests/types.test.ts new file mode 100644 index 000000000..ecc8f796d --- /dev/null +++ b/tests/types.test.ts @@ -0,0 +1,161 @@ +import { expectTypeOf } from 'expect-type'; +import { observable } from '../src/observable'; +import { + ObservableArray, + Observable, + ObservableObject, + ObservablePrimitive, +} from '../src/observableInterfaces'; +import { useObservableQuery } from 'src/react-hooks/useObservableQuery'; + +describe('Types', () => { + describe('observable', () => { + it('issue #151', () => { + type ObservableFn = ReturnType< + typeof observable<{ + optional?: { foo: string }; + nullable: { foo: string } | null; + }> + >; + + expectTypeOf().returns.toEqualTypeOf<{ + optional?: { foo: string }; + nullable: { foo: string } | null; + }>(); + expectTypeOf().returns.toEqualTypeOf(); + expectTypeOf().returns.toEqualTypeOf(); + }); + }); + + describe('Observable', () => { + describe('with state primitive', () => { + it('should infer string', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toBeString(); + }); + + it('should infer number', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toBeNumber(); + }); + + it('should infer boolean', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toBeBoolean(); + }); + + it('should infer null', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toBeNull(); + }); + + it('should infer undefined', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toBeUndefined(); + }); + }); + + describe('with state object', () => { + it('should infer object', () => { + type State = Observable<{ foo: string }>; + expectTypeOf().toMatchTypeOf>(); + expectTypeOf().not.toMatchTypeOf>(); + expectTypeOf().not.toMatchTypeOf>(); + expectTypeOf().returns.toBeObject(); + }); + + describe('with nested nullable types', () => { + it('should infer nested nullable value', () => { + type State = Observable<{ foo: { bar: string | null } }>; + expectTypeOf().returns.toEqualTypeOf(); + }); + + it('should infer nested optional value', () => { + type State = Observable<{ foo: { bar?: string } }>; + expectTypeOf().returns.toEqualTypeOf(); + }); + + it('should infer nested value as nullable if parent is nullable', () => { + type State = Observable<{ foo: { bar: string } | null }>; + expectTypeOf().returns.toEqualTypeOf(); + }); + + it('should infer nested value as optional if parent is optional', () => { + type State = Observable<{ foo?: { bar: string } }>; + expectTypeOf().returns.toEqualTypeOf(); + }); + + // Not sure if this is the desired behavior, what does legend state return in this scenario? + it('should infer nested value as both optional and nullable if their ancestors are', () => { + type State = Observable<{ foo?: { bar: { value: number } } | null }>; + expectTypeOf().returns.toEqualTypeOf< + number | undefined | null + >(); + }); + + // Not sure if this is the desired behavior, what does legend state return in this scenario? + it('should infer nested optional value as both optional and nullable if parent is nullable', () => { + type State = Observable<{ foo: { bar?: string | null }>; + expectTypeOf().returns.toEqualTypeOf< + string | undefined | null + >(); + }); + }); + + describe('with nested state primitive', () => { + it('should infer string', () => { + type GetState = Observable<{ foo: string }>['foo']['get']; + expectTypeOf().returns.toBeString(); + }); + + it('should infer number', () => { + type GetState = Observable<{ foo: number }>['foo']['get']; + expectTypeOf().returns.toBeNumber(); + }); + + it('should infer boolean', () => { + type GetState = Observable<{ foo: boolean }>['foo']['get']; + expectTypeOf().returns.toBeBoolean(); + }); + + it('should infer null', () => { + type GetState = Observable<{ foo: null }>['foo']['get']; + expectTypeOf().returns.toBeNull(); + }); + + it('should infer undefined', () => { + type GetState = Observable<{ foo: undefined }>['foo']['get']; + expectTypeOf().returns.toBeUndefined(); + }); + }); + }); + + it('should infer array', () => { + type GetState = Observable<{ foo: 'bar' }[]>; + expectTypeOf().toMatchTypeOf>(); + }); + + it('should infer Map', () => { + type GetState = Observable>['get']; + expectTypeOf().returns.toEqualTypeOf>(); + }); + + describe('with maybe undefined', () => { + it('with primitive', () => { + type GetState = Observable['get']; + expectTypeOf().returns.toEqualTypeOf(); + }); + + it('with object', () => { + type GetState = Observable<{ foo: string } | undefined>['get']; + expectTypeOf().returns.toEqualTypeOf<{ foo: string } | undefined>(); + }); + + it('with array', () => { + type GetState = Observable<{ foo: string }[] | undefined>['get']; + expectTypeOf().returns.toEqualTypeOf<{ foo: string }[] | undefined>(); + }); + }); + + }); +}); From cdf3256d422939f6653474fa876eb894951d0ffd Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 22:49:37 +0200 Subject: [PATCH 05/14] utilize `Extract` type utility --- src/observableInterfaces.ts | 10 +--------- tests/types.test.ts | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 7beadc4e3..7780e354e 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -348,18 +348,10 @@ export type ObservableMapIfMap = T extends Map | WeakMap export type ObservableArray = ObservableObjectFns & ObservableArrayOverride; export type ObservableObject = ObservableFnsRecursive< NonNullable, - PickNullableAndUndefined + Extract > & ObservableObjectFns; -export type PickNullableAndUndefined = T extends null - ? T extends undefined - ? null | undefined - : null - : T extends undefined - ? undefined - : never; - export type ObservableChild = [T] extends [Primitive] ? ObservablePrimitiveChild : ObservableObject; export type ObservablePrimitiveChild = [T] extends [boolean] ? ObservablePrimitiveChildFns & ObservablePrimitiveBooleanFns diff --git a/tests/types.test.ts b/tests/types.test.ts index ecc8f796d..11ebb6bce 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -6,7 +6,6 @@ import { ObservableObject, ObservablePrimitive, } from '../src/observableInterfaces'; -import { useObservableQuery } from 'src/react-hooks/useObservableQuery'; describe('Types', () => { describe('observable', () => { From 28054be535d7f69a8c89bbca15fe06867c784ca9 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 22:52:33 +0200 Subject: [PATCH 06/14] rename generic type `Nullable` to `NullableMarker` --- src/observableInterfaces.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 7780e354e..9d25f5690 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -124,20 +124,20 @@ type Recurse = T[K] extends ObservableReadable ? TRecurse : T[K]; -type ObservableFnsRecursiveUnsafe = { - [K in keyof T]-?: Recurse>; +type ObservableFnsRecursiveUnsafe = { + [K in keyof T]-?: Recurse>; }; -type ObservableFnsRecursiveSafe = { - readonly [K in keyof T]-?: Recurse>; +type ObservableFnsRecursiveSafe = { + readonly [K in keyof T]-?: Recurse>; }; -type MakeNullable = { - [K in keyof T]: T[K] | Nullable; +type MarkValuesNullable = { + [K in keyof T]: T[K] | NullableMarker; }; -type ObservableFnsRecursive = ObservableFnsRecursiveSafe, Nullable> & - ObservableFnsRecursiveUnsafe, Nullable>, Nullable>; +type ObservableFnsRecursive = ObservableFnsRecursiveSafe, NullableMarker> & + ObservableFnsRecursiveUnsafe, NullableMarker>, NullableMarker>; type ObservableComputedFnsRecursive = { readonly [K in keyof T]-?: Recurse>>; @@ -346,11 +346,11 @@ export type ObservableSet | WeakSet> = Omit & Omit, 'size'> & { size: ObservablePrimitiveChild }; export type ObservableMapIfMap = T extends Map | WeakMap ? ObservableMap : never; export type ObservableArray = ObservableObjectFns & ObservableArrayOverride; -export type ObservableObject = ObservableFnsRecursive< +export type ObservableObject = ObservableFnsRecursive< NonNullable, - Extract + Extract > & - ObservableObjectFns; + ObservableObjectFns; export type ObservableChild = [T] extends [Primitive] ? ObservablePrimitiveChild : ObservableObject; export type ObservablePrimitiveChild = [T] extends [boolean] From 3df1872f73aad78a93f9cca940fa655e72a0b868 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 23:05:17 +0200 Subject: [PATCH 07/14] fix syntax error --- tests/types.test.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/types.test.ts b/tests/types.test.ts index 11ebb6bce..25d3ffe80 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -1,11 +1,6 @@ import { expectTypeOf } from 'expect-type'; import { observable } from '../src/observable'; -import { - ObservableArray, - Observable, - ObservableObject, - ObservablePrimitive, -} from '../src/observableInterfaces'; +import { ObservableArray, Observable, ObservableObject, ObservablePrimitive } from '../src/observableInterfaces'; describe('Types', () => { describe('observable', () => { @@ -94,10 +89,8 @@ describe('Types', () => { // Not sure if this is the desired behavior, what does legend state return in this scenario? it('should infer nested optional value as both optional and nullable if parent is nullable', () => { - type State = Observable<{ foo: { bar?: string | null }>; - expectTypeOf().returns.toEqualTypeOf< - string | undefined | null - >(); + type State = Observable<{ foo: { bar?: string } | null }>; + expectTypeOf().returns.toEqualTypeOf(); }); }); @@ -155,6 +148,5 @@ describe('Types', () => { expectTypeOf().returns.toEqualTypeOf<{ foo: string }[] | undefined>(); }); }); - }); }); From d85502806104b24765181aa84f239689aede1112 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 23:06:25 +0200 Subject: [PATCH 08/14] fix type assertion test case --- tests/types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/types.test.ts b/tests/types.test.ts index 25d3ffe80..497ad32ce 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -81,7 +81,7 @@ describe('Types', () => { // Not sure if this is the desired behavior, what does legend state return in this scenario? it('should infer nested value as both optional and nullable if their ancestors are', () => { - type State = Observable<{ foo?: { bar: { value: number } } | null }>; + type State = Observable<{ foo?: { bar: { value: number } | null } }>; expectTypeOf().returns.toEqualTypeOf< number | undefined | null >(); From 00055487343544912d4be807916dabdc78d7be77 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Thu, 14 Sep 2023 23:09:34 +0200 Subject: [PATCH 09/14] Rename 'PrimitiveKeys' to 'PrimitiveProps' --- src/observableInterfaces.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 9d25f5690..3c02b920d 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -95,8 +95,8 @@ export interface ListenerParams { } export type ListenerFn = (params: ListenerParams) => void; -type PrimitiveKeys = Pick; -type StripPrimitiveProps = Pick; +type PrimitiveProps = Pick; +type NonPrimitiveProps = Pick; type Recurse = T[K] extends ObservableReadable ? T[K] @@ -136,8 +136,8 @@ type MarkValuesNullable = { [K in keyof T]: T[K] | NullableMarker; }; -type ObservableFnsRecursive = ObservableFnsRecursiveSafe, NullableMarker> & - ObservableFnsRecursiveUnsafe, NullableMarker>, NullableMarker>; +type ObservableFnsRecursive = ObservableFnsRecursiveSafe, NullableMarker> & + ObservableFnsRecursiveUnsafe, NullableMarker>, NullableMarker>; type ObservableComputedFnsRecursive = { readonly [K in keyof T]-?: Recurse>>; From 707ba793019a080f06cbf3d16d91246623e9ee42 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Mon, 18 Sep 2023 19:19:16 +0200 Subject: [PATCH 10/14] value is undefined if one of its ancestors is null --- src/observableInterfaces.ts | 8 ++++---- tests/types.test.ts | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index 3c02b920d..ac8dbf826 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -132,12 +132,12 @@ type ObservableFnsRecursiveSafe = { readonly [K in keyof T]-?: Recurse>; }; -type MarkValuesNullable = { - [K in keyof T]: T[K] | NullableMarker; +type MakeNullable = { + [K in keyof T]: T[K] | (NullableMarker extends never ? never : undefined); }; type ObservableFnsRecursive = ObservableFnsRecursiveSafe, NullableMarker> & - ObservableFnsRecursiveUnsafe, NullableMarker>, NullableMarker>; + ObservableFnsRecursiveUnsafe, NullableMarker>, NullableMarker>; type ObservableComputedFnsRecursive = { readonly [K in keyof T]-?: Recurse>>; @@ -350,7 +350,7 @@ export type ObservableObject = ObservableFnsRec NonNullable, Extract > & - ObservableObjectFns; + ObservableObjectFns; export type ObservableChild = [T] extends [Primitive] ? ObservablePrimitiveChild : ObservableObject; export type ObservablePrimitiveChild = [T] extends [boolean] diff --git a/tests/types.test.ts b/tests/types.test.ts index 497ad32ce..d8154cc96 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -16,7 +16,9 @@ describe('Types', () => { optional?: { foo: string }; nullable: { foo: string } | null; }>(); - expectTypeOf().returns.toEqualTypeOf(); + + // Note that if a parent is nullable, the child is optional (undefined) + expectTypeOf().returns.toEqualTypeOf(); expectTypeOf().returns.toEqualTypeOf(); }); }); @@ -69,9 +71,9 @@ describe('Types', () => { expectTypeOf().returns.toEqualTypeOf(); }); - it('should infer nested value as nullable if parent is nullable', () => { + it('should infer nested value as optional if parent is nullable', () => { type State = Observable<{ foo: { bar: string } | null }>; - expectTypeOf().returns.toEqualTypeOf(); + expectTypeOf().returns.toEqualTypeOf(); }); it('should infer nested value as optional if parent is optional', () => { @@ -79,17 +81,18 @@ describe('Types', () => { expectTypeOf().returns.toEqualTypeOf(); }); - // Not sure if this is the desired behavior, what does legend state return in this scenario? - it('should infer nested value as both optional and nullable if their ancestors are', () => { + it('should infer nested value as optional if their ancestors are optional and nullable', () => { type State = Observable<{ foo?: { bar: { value: number } | null } }>; - expectTypeOf().returns.toEqualTypeOf< - number | undefined | null - >(); + expectTypeOf().returns.toEqualTypeOf(); }); - // Not sure if this is the desired behavior, what does legend state return in this scenario? - it('should infer nested optional value as both optional and nullable if parent is nullable', () => { + it('should infer nullable value as both nullable and optional if parent is nullable', () => { type State = Observable<{ foo: { bar?: string } | null }>; + expectTypeOf().returns.toEqualTypeOf(); + }); + + it('should infer nullable value as both nullable and optional if parent is optional', () => { + type State = Observable<{ foo?: { bar: string | null } }>; expectTypeOf().returns.toEqualTypeOf(); }); }); From 70ee363f64dd50d597497f579a70efb86eae7842 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Mon, 18 Sep 2023 19:27:17 +0200 Subject: [PATCH 11/14] cleanup utility types for filtering keys --- src/observableInterfaces.ts | 51 +++++++++++++------------------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index ac8dbf826..614d63fd5 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -95,8 +95,8 @@ export interface ListenerParams { } export type ListenerFn = (params: ListenerParams) => void; -type PrimitiveProps = Pick; -type NonPrimitiveProps = Pick; +type PrimitiveProps = Pick>; +type NonPrimitiveProps = Omit>; type Recurse = T[K] extends ObservableReadable ? T[K] @@ -279,48 +279,31 @@ export type RecordValue = T extends Record ? t : never; export type ArrayValue = T extends Array ? t : never; export type ObservableValue = T extends Observable ? t : never; -// This converts the state object's shape to the field transformer's shape -// TODO: FieldTransformer and this shape can likely be refactored to be simpler -declare type ObjectKeys = Pick< - T, - { - [K in keyof T]-?: K extends string - ? T[K] extends Record - ? T[K] extends any[] - ? never - : K - : never - : never; - }[keyof T] ->; -declare type DictKeys = Pick< - T, - { - [K in keyof T]-?: K extends string ? (T[K] extends Record> ? K : never) : never; - }[keyof T] ->; -declare type ArrayKeys = Pick< - T, - { - [K in keyof T]-?: K extends string | number ? (T[K] extends any[] ? K : never) : never; - }[keyof T] ->; -export declare type FieldTransforms = +type FilterKeysByValue = { + [K in keyof T]: T[K] extends U ? K & (string | number) : never; +}[keyof T]; + +type ObjectKeys = Exclude>, FilterKeysByValue>; +type DictKeys = FilterKeysByValue>>; +type ArrayKeys = FilterKeysByValue; +type PrimitiveKeys = FilterKeysByValue; + +export type FieldTransforms = | (T extends Record> ? { _dict: FieldTransformsInner> } : never) | FieldTransformsInner; -export declare type FieldTransformsInner = { +export type FieldTransformsInner = { [K in keyof T]: string; } & ( | { - [K in keyof ObjectKeys as `${K}_obj`]?: FieldTransforms; + [K in ObjectKeys as `${K}_obj`]?: FieldTransforms; } | { - [K in keyof DictKeys as `${K}_dict`]?: FieldTransforms>; + [K in DictKeys as `${K}_dict`]?: FieldTransforms>; } ) & { - [K in keyof ArrayKeys as `${K}_arr`]?: FieldTransforms>; + [K in ArrayKeys as `${K}_arr`]?: FieldTransforms>; } & { - [K in keyof ArrayKeys as `${K}_val`]?: FieldTransforms>; + [K in ArrayKeys as `${K}_val`]?: FieldTransforms>; }; export type Selector = ObservableReadable | ObservableEvent | (() => T) | T; From 9ca285d9daa45afbd91419b8eb5d9d516c56791e Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Tue, 19 Sep 2023 00:23:20 +0200 Subject: [PATCH 12/14] remove explicit function overloads for promises --- src/observable.ts | 4 ---- tests/tests.test.ts | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/observable.ts b/src/observable.ts index fc1861bc5..9272a3e22 100644 --- a/src/observable.ts +++ b/src/observable.ts @@ -35,16 +35,12 @@ function createObservable(value: T, makePrimitive: boolean): Observable | export type MaybePromiseObservable = Observable>; export function observable(): MaybePromiseObservable; -export function observable(value: Promise): MaybePromiseObservable>; -export function observable(value?: Promise): MaybePromiseObservable | undefined>; export function observable(value: T): MaybePromiseObservable; export function observable(value?: T): MaybePromiseObservable { return createObservable(value, /*makePrimitive*/ false); } export function observablePrimitive(): ObservablePrimitive>; -export function observablePrimitive(value: Promise): ObservablePrimitive>>; -export function observablePrimitive(value?: Promise): ObservablePrimitive> | undefined>; export function observablePrimitive(value: T): ObservablePrimitive>; export function observablePrimitive(value?: T): ObservablePrimitive> { return createObservable(value, /*makePrimitive*/ true); diff --git a/tests/tests.test.ts b/tests/tests.test.ts index a3634a970..3d9822d8b 100644 --- a/tests/tests.test.ts +++ b/tests/tests.test.ts @@ -2195,7 +2195,7 @@ describe('Promise values', () => { test('when callback works with promises', async () => { let resolver: (value: number) => void; const promise = new Promise((resolve) => (resolver = resolve)); - const obs = observable(promise); + const obs = observable(promise); let didWhen = false; when(obs, () => { didWhen = true; @@ -2208,7 +2208,7 @@ describe('Promise values', () => { test('when works with promises', async () => { let resolver: (value: number) => void; const promise = new Promise((resolve) => (resolver = resolve)); - const obs = observable(promise); + const obs = observable(promise); let didWhen = false; when(obs).then(() => { didWhen = true; @@ -2564,7 +2564,7 @@ describe('Locking', () => { }); describe('Primitive <-> Object', () => { test('Starting as undefined', () => { - const obs = observable<{ test: string }>(undefined); + const obs = observable<{ test: string } | undefined>(undefined); expect(obs.get()).toEqual(undefined); obs.set({ test: 'hi' }); expect(obs.get()).toEqual({ test: 'hi' }); From 9ff58fffabf530c7a84270318cf831ae4f86b6c0 Mon Sep 17 00:00:00 2001 From: Bram Hoendervangers Date: Tue, 19 Sep 2023 00:47:41 +0200 Subject: [PATCH 13/14] cleanup type in createObservable --- src/observable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/observable.ts b/src/observable.ts index 9272a3e22..964ca73ce 100644 --- a/src/observable.ts +++ b/src/observable.ts @@ -22,7 +22,7 @@ function createObservable(value: T, makePrimitive: boolean): Observable | const prim = makePrimitive || isActualPrimitive(value); const obs = prim - ? (new (ObservablePrimitiveClass as any)(node) as Observable) + ? (new (ObservablePrimitiveClass as any)(node) as ObservablePrimitive) : (getProxy(node) as Observable); if (valueIsPromise) { From d7bbbafa98c6b01db21521f7e7a003778da7ebbc Mon Sep 17 00:00:00 2001 From: Jay Meistrich Date: Tue, 19 Sep 2023 14:03:36 +0200 Subject: [PATCH 14/14] fix: imports from 'src/..." --- src/react/useObservable.ts | 2 +- src/react/useObservableReducer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react/useObservable.ts b/src/react/useObservable.ts index 621557c1a..c04e00119 100644 --- a/src/react/useObservable.ts +++ b/src/react/useObservable.ts @@ -1,6 +1,6 @@ import { isFunction, observable } from '@legendapp/state'; import { useMemo } from 'react'; -import { MaybePromiseObservable } from 'src/observable'; +import type { MaybePromiseObservable } from '../observable'; /** * A React hook that creates a new observable diff --git a/src/react/useObservableReducer.ts b/src/react/useObservableReducer.ts index 4e4f93bfc..ed4d6bd4e 100644 --- a/src/react/useObservableReducer.ts +++ b/src/react/useObservableReducer.ts @@ -10,7 +10,7 @@ import type { ReducerWithoutAction, } from 'react'; import { useObservable } from './useObservable'; -import { MaybePromiseObservable } from 'src/observable'; +import type { MaybePromiseObservable } from '../observable'; export function useObservableReducer, I>( reducer: R,