mobx-state-tree
mobx-state-tree copied to clipboard
typed environment at model definition
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
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 perhaps the idea of contexts from mobx-keystone could be used for this https://mobx-keystone.js.org/contexts
@mweststrate is there a sample for the utility? or a library around that use case?