'How to type annotate MobX observable objects using non-OOP approach?

Please, consider this example:

import { observable } from "mobx";

interface MyObject {
  name: string;
  age: number;
}

const obj1 = { namee: "Uldis", age: "35" };

type MyObjectObservable = ???;

const obj1Observable: MyObjectObservable = observable(obj1); // I want to have compiler error here

function doStuff(obj: MyObjectObservable) {}

doStuff({ name: "Uldis", age: 35 }) // I want to have compiler error here because passed object is not an observable

How should type for MyObjectObservable be defined? I want to have as strict types as possible and I don't want to use OOP approach because of other type related issues such as not having precise subtypes for the state that methods use.



Solution 1:[1]

There should be no error in Typescript when using observable or makeObservable

This is all valid code:

import { makeAutoObservable, observable } from "mobx"

interface MyObject {
  name: string
  age: number
}

const obj1 = { name: "Uldis", age: 35 }

const make: MyObject = makeAutoObservable(obj1)
const obj1Observable: MyObject = observable(obj1)

function doStuff(obj: MyObject) {}

doStuff({ name: "Uldis", age: 35 })
doStuff(make)
doStuff(obj1Observable)

Solution 2:[2]

I see two possible solutions:

  1. Use boxed observable
import { observable, IObservableValue } from "mobx";

interface MyObject {
  name: string;
  age: number;
}

const obj1 = { namee: "Uldis", age: "35" };

type MyObjectObservable = IObservableValue<MyObject>;

const obj1Observable: MyObjectObservable = observable.box(obj1);

obj1Observable.get(); // returns MyObject
  1. Add your own branded Observable type in order to avoid wrapping with an additional object.
//customObservable.ts

import {observable as observableImpl} from 'mobx'

const mobx = Symbol('mobx');
export type Observable<T> = T & { [K in typeof mobx]: never };

export const observable = <T extends object>(obj: T): Observable<T> => {
  return observableImpl(obj) as Observable<T>;
};

import { observable, Observable } from "./customObservable";
import { observable as originalObservable} from "mobx";

interface MyObject {
  name: string;
  age: number;
}

const obj1 = { namee: "Uldis", age: "35" };

type MyObjectObservable = Observable<MyObject>;

const obj1Observable: MyObjectObservable = observable(obj1);
const obj2Observable: MyObjectObservable = originalObservable(obj1); // compile-time error

obj1Observable.name; //string
obj1Observable.age;  //number

Solution 3:[3]

After looking at this more closely I've came up with this solution:

import { makeObservable,IObservableValue } from "mobx"

type MyObservable=IObservableValue<{name:string}>

const obj = makeMyObservable({ name:'Johny' })
const obj2 = {name:'Johny'}

function makeMyObservable<T extends Record<string,any>>(val:T){

    return makeObservable(val) as unknown as IObservableValue<T>
}

function onlyMyObservable(val:MyObservable){
    return val
}

onlyMyObservable(obj)
onlyMyObservable(obj2) // error

TS Playground

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ivan V.
Solution 2 robak86
Solution 3 Ivan V.