diff --git a/.changeset/fluffy-shoes-return.md b/.changeset/fluffy-shoes-return.md new file mode 100644 index 000000000..e3189a75a --- /dev/null +++ b/.changeset/fluffy-shoes-return.md @@ -0,0 +1,13 @@ +--- +"mobx": major +--- + +Only expose IComputedValue.set as a typescript type when it won't throw an error + +The breaking change is that it is now a typescript compile error to call the `set(value: T)` method on an `IComputedValue` that is not writable. +This is a breaking change because it is possible to create an `IComputedValue` that is not writable, and then call `set` on it. +This will now throw a runtime error. + +This change was made to help with refactoring code, especially between `IObservableValue` and `IComputedValue` which both have a similar interface but one of them throws more errors. + +To fix your code, you will need to change the type annotations for any updatable computed value to `IComputedValue`. diff --git a/docs/computeds.md b/docs/computeds.md index 45befc1ad..22672f7bd 100644 --- a/docs/computeds.md +++ b/docs/computeds.md @@ -236,3 +236,8 @@ It is recommended to set this one to `true` on very expensive computed values. I ### `keepAlive` This avoids suspending computed values when they are not being observed by anything (see the above explanation). Can potentially create memory leaks, similar to the ones discussed for [reactions](reactions.md#always-dispose-of-reactions). + +### `set` + +This optional function allows the returned `IComputedValue` to also act as something that knows how to update its backing data. When not provided, however, this function that is provided will throw an error. +To help prevent runtime errors like this, especially when refactoring code from `IObservableValue` to `IComputedValue` the typescript types will are set up so that trying to call a function will result in a compile time error when this option is not set. diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts index e7a3a510e..1b2cc5dcd 100644 --- a/packages/mobx/__tests__/v5/base/typescript-tests.ts +++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts @@ -2474,3 +2474,20 @@ test("observable.box should keep track of undefined and null in type", () => { const a = observable.box() assert>>(true) }) + +test("computed without a set function should not allow the consumer to set the value", () => { + const a = observable.box("hello") + const b = computed(() => a.get()) + assert>>(true) + // @ts-expect-error + b.set("world") +}) + +test("computed with a set function should allow the consumer to set the value", () => { + const a = observable.box("hello") + const b = computed(() => a.get(), { + set: value => a.set(value) + }) + assert>>(true) + b.set("world") +}) diff --git a/packages/mobx/src/api/computed.ts b/packages/mobx/src/api/computed.ts index 104932bd3..8d982e7c6 100644 --- a/packages/mobx/src/api/computed.ts +++ b/packages/mobx/src/api/computed.ts @@ -18,9 +18,11 @@ export const COMPUTED_STRUCT = "computed.struct" export interface IComputedFactory extends Annotation, PropertyDecorator { // @computed(opts) - (options: IComputedValueOptions): Annotation & PropertyDecorator + (options: IComputedValueOptions): Annotation & PropertyDecorator // computed(fn, opts) - (func: () => T, options?: IComputedValueOptions): IComputedValue + (func: () => T, options?: IComputedValueOptions): IComputedValue + (func: () => T, options?: IComputedValueOptions): IComputedValue + (func: () => T, options?: IComputedValueOptions): IComputedValue struct: Annotation & PropertyDecorator } diff --git a/packages/mobx/src/core/computedvalue.ts b/packages/mobx/src/core/computedvalue.ts index a56d59525..de0c06aec 100644 --- a/packages/mobx/src/core/computedvalue.ts +++ b/packages/mobx/src/core/computedvalue.ts @@ -32,20 +32,19 @@ import { allowStateChangesEnd } from "../internal" -export interface IComputedValue { +export interface IComputedValue { get(): T - set(value: T): void + set: CanUpdate extends true ? (value: T) => void : undefined } -export interface IComputedValueOptions { +export type IComputedValueOptions = { get?: () => T - set?: (value: T) => void name?: string equals?: IEqualsComparer context?: any requiresReaction?: boolean keepAlive?: boolean -} +} & (CanUpdate extends true ? { set: (value: T) => void } : { set?: undefined }) export type IComputedDidChange = { type: "update" @@ -75,7 +74,7 @@ export type IComputedDidChange = { * * If at any point it's outside batch and it isn't observed: reset everything and go to 1. */ -export class ComputedValue implements IObservable, IComputedValue, IDerivation { +export class ComputedValue implements IObservable, IComputedValue, IDerivation { dependenciesState_ = IDerivationState_.NOT_TRACKING_ observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes newObserving_ = null // during tracking it's an array with new observed observers @@ -112,7 +111,7 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva * don't want to notify observers if it is structurally the same. * This is useful for working with vectors, mouse coordinates etc. */ - constructor(options: IComputedValueOptions) { + constructor(options: IComputedValueOptions) { if (!options.get) { die(31) } diff --git a/packages/mobx/src/mobx.ts b/packages/mobx/src/mobx.ts index cd7cb05af..50aaea051 100644 --- a/packages/mobx/src/mobx.ts +++ b/packages/mobx/src/mobx.ts @@ -129,7 +129,6 @@ export { onReactionError, interceptReads as _interceptReads, IComputedValueOptions, - IActionRunInfo, _startAction, _endAction, allowStateReadsStart as _allowStateReadsStart,