mobxjs / mobx-state-tree

Full-featured reactive state management without the boilerplate
https://mobx-state-tree.js.org/
MIT License
6.99k stars 641 forks source link

typed environment at model definition #1359

Open pravinbashyal opened 5 years ago

pravinbashyal commented 5 years ago

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?

mweststrate commented 5 years ago

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)

xaviergonz commented 5 years ago

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

pravinbashyal commented 5 years ago

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