mobx-state-tree icon indicating copy to clipboard operation
mobx-state-tree copied to clipboard

typed environment at model definition

Open pravinbashyal opened this issue 6 years ago • 3 comments

Feature request

Is your feature request related to a problem? Please describe.

When I have to instantiate a model with env, I have to define the interface for the env at the point of instantiation. Take the following code for example


import { types, getEnv } from 'mobx-state-tree'

interface ITranslatorService {
  lang: 'en' | 'de'
  translate: (token: string, lang: 'en' | 'de') => string
}

interface IModelA {
  token1: string
}

// definition
export const ModelA = types
  .model<IModelA>('ModelA', {
    token1: types.string,
  })
  .views(self => ({
    get prop1Translated(): string {
      const { translate, lang } = getEnv<ITranslatorService>(self)
      return translate(self.token1, lang)
    },
  }))

export type TModelA = typeof ModelA.Type

// usage
try {
  const modelA1 = ModelA.create({ token1: 'someText' })
  console.log(modelA1.prop1Translated) // throws error at runtime but not compiletime translate is not a function
} catch (e) {
  console.log(e)
}

const modelA2 = ModelA.create(
  { token1: 'someText' },
  {
    lang: 'en',
    translate: (token: string) => token,
  }
)
console.log(modelA2.prop1Translated) // runs correctly

Describe the solution you'd like

Extend types.model<T> to accomodate optional dependencies types.model<T, DependenciesInterface?> so the error will be thrown at compiletime as follows:

import { types, getEnv } from 'mobx-state-tree'

interface ITranslatorService {
  lang: 'en' | 'de'
  translate: (token: string, lang: 'en' | 'de') => string
}

interface IModelA {
  token1: string
}

// definition
export const ModelA = types
  .model<IModelA, ITranslatorService>('ModelA', { // types.model<T, EnvInterface>
    token1: types.string,
  })
  .views(self => ({
    get prop1Translated(): string {
      const { lastUsedLanguage } = getEnv(self) // throws error
      const { translate, lang } = getEnv(self)
      return translate(self.token1, lang)
    },
  }))

export type TModelA = typeof ModelA.Type

// usage
try {
  const modelA1 = ModelA.create({ token1: 'someText' }) // throws error at compiletime because no dependency provided
  console.log(modelA1.prop1Translated) 
} catch (e) {
  console.log(e)
}

const modelA2 = ModelA.create( // runs correctly
  { token1: 'someText' },
  {
    lang: 'en',
    translate: (token: string) => token,
  }
)
console.log(modelA2.prop1Translated) // runs correctly

Describe alternatives you've considered

Additional context

Are you willing to (attempt) a PR?

  • [ ] Yes
  • [x] No

pravinbashyal avatar Aug 02 '19 13:08 pravinbashyal

I played shortly with that idea in the past, but it makes the already troublesome dealing of TS with circular types even more horrendous. Instead, I suggest to use getEnv<Interface>() in your implementation. If you get tired of repeating that, just make a utility function / view for that, so that you have to do it only once. (Some people create their own models for this, and compose these views into the actual models, so that the same utility view definition can be used in many places)

mweststrate avatar Aug 05 '19 20:08 mweststrate

@mweststrate perhaps the idea of contexts from mobx-keystone could be used for this https://mobx-keystone.js.org/contexts

xaviergonz avatar Sep 21 '19 20:09 xaviergonz

@mweststrate is there a sample for the utility? or a library around that use case?

pravinbashyal avatar Oct 09 '19 12:10 pravinbashyal