Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: Prevent calling set on the return value of 'computed()' #3783

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions .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<T>` and `IComputedValue<T>` 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<T, true>`.
5 changes: 5 additions & 0 deletions docs/computeds.md
Expand Up @@ -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<T>` 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<T>` to `IComputedValue<T>` 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.
17 changes: 17 additions & 0 deletions packages/mobx/__tests__/v5/base/typescript-tests.ts
Expand Up @@ -2474,3 +2474,20 @@ test("observable.box should keep track of undefined and null in type", () => {
const a = observable.box<string | undefined>()
assert<IsExact<typeof a, IObservableValue<string | undefined>>>(true)
})

test("computed without a set function should not allow the consumer to set the value", () => {
const a = observable.box<string>("hello")
const b = computed(() => a.get())
assert<IsExact<typeof b, mobx.IComputedValue<string>>>(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<string>("hello")
const b = computed(() => a.get(), {
set: value => a.set(value)
})
assert<IsExact<typeof b, mobx.IComputedValue<string, true>>>(true)
b.set("world")
})
6 changes: 4 additions & 2 deletions packages/mobx/src/api/computed.ts
Expand Up @@ -18,9 +18,11 @@ export const COMPUTED_STRUCT = "computed.struct"

export interface IComputedFactory extends Annotation, PropertyDecorator {
// @computed(opts)
<T>(options: IComputedValueOptions<T>): Annotation & PropertyDecorator
<T>(options: IComputedValueOptions<T, boolean>): Annotation & PropertyDecorator
// computed(fn, opts)
<T>(func: () => T, options?: IComputedValueOptions<T>): IComputedValue<T>
<T>(func: () => T, options?: IComputedValueOptions<T, false>): IComputedValue<T, false>
<T>(func: () => T, options?: IComputedValueOptions<T, true>): IComputedValue<T, true>
<T>(func: () => T, options?: IComputedValueOptions<T, boolean>): IComputedValue<T, boolean>

struct: Annotation & PropertyDecorator
}
Expand Down
13 changes: 6 additions & 7 deletions packages/mobx/src/core/computedvalue.ts
Expand Up @@ -32,20 +32,19 @@ import {
allowStateChangesEnd
} from "../internal"

export interface IComputedValue<T> {
export interface IComputedValue<T, CanUpdate extends boolean = false> {
get(): T
set(value: T): void
set: CanUpdate extends true ? (value: T) => void : undefined
}

export interface IComputedValueOptions<T> {
export type IComputedValueOptions<T, CanUpdate extends boolean = false> = {
get?: () => T
set?: (value: T) => void
name?: string
equals?: IEqualsComparer<T>
context?: any
requiresReaction?: boolean
keepAlive?: boolean
}
} & (CanUpdate extends true ? { set: (value: T) => void } : { set?: undefined })

export type IComputedDidChange<T = any> = {
type: "update"
Expand Down Expand Up @@ -75,7 +74,7 @@ export type IComputedDidChange<T = any> = {
*
* If at any point it's outside batch and it isn't observed: reset everything and go to 1.
*/
export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation {
export class ComputedValue<T> implements IObservable, IComputedValue<T, boolean>, 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
Expand Down Expand Up @@ -112,7 +111,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, 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<T>) {
constructor(options: IComputedValueOptions<T, boolean>) {
if (!options.get) {
die(31)
}
Expand Down
1 change: 0 additions & 1 deletion packages/mobx/src/mobx.ts
Expand Up @@ -129,7 +129,6 @@ export {
onReactionError,
interceptReads as _interceptReads,
IComputedValueOptions,
IActionRunInfo,
_startAction,
_endAction,
allowStateReadsStart as _allowStateReadsStart,
Expand Down