From 74f26b59260c8840790fd9a8c4763ec882c9f760 Mon Sep 17 00:00:00 2001 From: Michael Best Date: Fri, 26 Sep 2025 01:56:31 -0700 Subject: [PATCH 1/2] fix: Fix issue with wrong inferred type of observable.map in TS 5.9+ (#4578) * Fix issue with wrong inferred type of observable.map in TS 5.9+ * initialValues can be optional --- package.json | 4 +- .../__tests__/v5/base/typescript-tests.ts | 38 +++++++++++++++++++ packages/mobx/src/api/observable.ts | 26 +++++++++++-- packages/mobx/src/types/observablemap.ts | 2 +- yarn.lock | 8 ++-- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 94123f577..b6ea9ee0d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ ], "resolutions": { "jest": "^29.5.0", - "typescript": "^5.6.2", + "typescript": "^5.9.2", "recast": "^0.23.1" }, "repository": { @@ -68,7 +68,7 @@ "tape": "^5.0.1", "ts-jest": "^29.0.5", "tsdx": "^0.14.1", - "typescript": "^5.6.2" + "typescript": "^5.9.2" }, "husky": { "hooks": { diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts index e7a3a510e..8c17f046d 100644 --- a/packages/mobx/__tests__/v5/base/typescript-tests.ts +++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts @@ -1933,6 +1933,44 @@ test("allow 'as const' for creating tuples for observable.map()", () => { mobx.observable.map(newValues) }) +test("infers tuple type for observable.map() when using array.map", () => { + interface Usage { + daily: number + monthly: number + } + const data: Array<{ id: string; usage: Usage }> = [ + { id: "1", usage: { daily: 5, monthly: 100 } }, + { id: "2", usage: { daily: 10, monthly: 200 } }, + { id: "3", usage: { daily: 15, monthly: 300 } } + ] + + const map = mobx.observable.map( + data.map(app => [app.id, app.usage]), + { name: "test" } + ) + + expect(map.get("2")).toEqual({ daily: 10, monthly: 200 }) +}) + +test("observable.map() accepts an undefined value with or without options", () => { + const map = mobx.observable.map(undefined, { name: "custom name" }) + map.set("test", 1) + expect(map.get("test")).toBe(1) + expect(map.name_).toBe("custom name") + + // without options + mobx.observable.map(undefined) +}) + +test("observable.map() accepts optional initial values", () => { + interface Usage {} + + ;(data?: [string, Usage][]) => observable.map(data, { name: "test" }) + ;(data?: readonly (readonly [string, Usage])[]) => observable.map(data, { name: "test" }) + ;(data?: Record) => observable.map(data, { name: "test" }) + ;(data?: Map) => observable.map(data, { name: "test" }) +}) + test("toJS bug #1413 (TS)", () => { class X { test = { diff --git a/packages/mobx/src/api/observable.ts b/packages/mobx/src/api/observable.ts index 47a77c248..738410ea6 100644 --- a/packages/mobx/src/api/observable.ts +++ b/packages/mobx/src/api/observable.ts @@ -3,6 +3,9 @@ import { IEqualsComparer, IObservableArray, IObservableMapInitialValues, + IMapEntries, + IReadonlyMapEntries, + IKeyValueMap, IObservableSetInitialValues, IObservableValue, ObservableMap, @@ -151,6 +154,24 @@ export interface IObservableValueFactory { (value?: T, options?: CreateObservableOptions): IObservableValue } +export interface IObservableMapFactory { + (): ObservableMap + (initialValues?: IMapEntries, options?: CreateObservableOptions): ObservableMap< + K, + V + > + ( + initialValues?: IReadonlyMapEntries, + options?: CreateObservableOptions + ): ObservableMap + (initialValues?: IKeyValueMap, options?: CreateObservableOptions): ObservableMap + (initialValues?: Map, options?: CreateObservableOptions): ObservableMap + (initialValues: undefined, options?: CreateObservableOptions): ObservableMap< + K, + V + > +} + export interface IObservableFactory extends Annotation, PropertyDecorator, @@ -172,10 +193,7 @@ export interface IObservableFactory initialValues?: IObservableSetInitialValues, options?: CreateObservableOptions ) => ObservableSet - map: ( - initialValues?: IObservableMapInitialValues, - options?: CreateObservableOptions - ) => ObservableMap + map: IObservableMapFactory object: ( props: T, decorators?: AnnotationsMap, diff --git a/packages/mobx/src/types/observablemap.ts b/packages/mobx/src/types/observablemap.ts index e47eead41..014b65507 100644 --- a/packages/mobx/src/types/observablemap.ts +++ b/packages/mobx/src/types/observablemap.ts @@ -46,7 +46,7 @@ export interface IKeyValueMap { export type IMapEntry = [K, V] export type IReadonlyMapEntry = readonly [K, V] export type IMapEntries = IMapEntry[] -export type IReadonlyMapEntries = IReadonlyMapEntry[] +export type IReadonlyMapEntries = readonly IReadonlyMapEntry[] export type IMapDidChange = { observableKind: "map"; debugObjectName: string } & ( | { diff --git a/yarn.lock b/yarn.lock index f4cf053ae..00d2cd90c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13778,10 +13778,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^3.7.3, typescript@^5.6.2: - version "5.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" - integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +typescript@^3.7.3, typescript@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== uglify-js@^3.1.4: version "3.17.4" From 76cad6fb0de75eda7568581acb278d14490eead4 Mon Sep 17 00:00:00 2001 From: Michael Best Date: Fri, 26 Sep 2025 01:59:02 -0700 Subject: [PATCH 2/2] fix: Rearrange observer type definitions to allow wrapping custom HOC (#4576) --- .../__tests__/observer.test.tsx | 27 +++++++++++++++++++ packages/mobx-react-lite/src/observer.ts | 11 ++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/mobx-react-lite/__tests__/observer.test.tsx b/packages/mobx-react-lite/__tests__/observer.test.tsx index f4f0a92b3..aeb82040a 100644 --- a/packages/mobx-react-lite/__tests__/observer.test.tsx +++ b/packages/mobx-react-lite/__tests__/observer.test.tsx @@ -594,6 +594,33 @@ test("useImperativeHandle and forwardRef should work with useObserver", () => { expect(consoleWarnMock).toMatchSnapshot() }) +test("observer should work with custom HOC and infer correct type", () => { + const validateChildrenWrapper = (component: React.FC): React.FC => { + const wrapper: React.FC = (...args) => { + const result = component(...args) + if (result && React.isValidElement(result) && result.type === "div") { + return result + } + throw new Error("Only
allowed as root element") + } + wrapper.displayName = component.displayName || component.name + return wrapper + } + + interface IProps { + value: string + } + + const TestComponent: React.FC = observer( + validateChildrenWrapper(function TestComponent({ value }) { + return
{value}
+ }) + ) + + const rendered = render() + expect(rendered.container.querySelector("div")!.innerHTML).toBe("1") +}) + it("should hoist known statics only", () => { function isNumber() { return null diff --git a/packages/mobx-react-lite/src/observer.ts b/packages/mobx-react-lite/src/observer.ts index 2f4a79928..6d9948d63 100644 --- a/packages/mobx-react-lite/src/observer.ts +++ b/packages/mobx-react-lite/src/observer.ts @@ -44,6 +44,11 @@ export function observer

( React.ForwardRefExoticComponent & React.RefAttributes> > +export function observer

( + baseComponent: React.FunctionComponent

, + options?: IObserverOptions +): React.FunctionComponent

+ export function observer

( baseComponent: React.ForwardRefExoticComponent< React.PropsWithoutRef

& React.RefAttributes @@ -51,12 +56,6 @@ export function observer

( ): React.MemoExoticComponent< React.ForwardRefExoticComponent & React.RefAttributes> > - -export function observer

( - baseComponent: React.FunctionComponent

, - options?: IObserverOptions -): React.FunctionComponent

- export function observer< C extends React.FunctionComponent | React.ForwardRefRenderFunction, Options extends IObserverOptions