-
-
Notifications
You must be signed in to change notification settings - Fork 348
[Question] How to get typesafe injection #256
Comments
Not sure if you're aware, but you can tell TypeScript that you know something is defined by using the following syntax (note the const { storeA } = props;
storeA!.something; See here. Makes this a lot simpler. There is the same problem in a lot of a React libraries that inject props (including redux, react-dnd). It's certainly a TypeScript-specific issue. |
The current declaration of React components don't allow this behavior. The only way to properly fix it is to define E.g.: Hope that makes sense. Not much can be done about it from mobx-react side of things. |
Not sure if it's of any help, but I just started picking up MobX and decided to go with a custom export function connect<MappedProps> (
mapStoreToProps: (store: Store) => MappedProps
) {
return function<WrappedProps> (
WrappedComponent: React.ComponentClass<WrappedProps> | React.StatelessComponent<WrappedProps>,
) {
const ConnectedComponent = inject(mapStoreToProps)(
observer(WrappedComponent)
) as React.ComponentClass<Partial<WrappedProps>>
return ConnectedComponent
}
}
// use as:
export default connect((store) => ({
session: store.session,
}))(MyComponent) With this, |
Any news about this?
It works, but its messy. |
There is an article on this topic, yet another way to deal with this https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf |
I'd add that the bang nullifies any existing type inference. |
I think the problem is basically that the React typings are too limited. If somebody would be willing to improve the react typings this could be easily addressed. Basically a component needs 3 generic arguments, instead of 2:
In that case you could have a component with the following signature:
I am wondering btw whether
|
I think the root of the problem is in how they implemented JSX support. currently it just uses the anyhow, I have a different approach using this type: type MyProps<P, T> = {props:Readonly<{ children?: ReactNode }> & Readonly<P>} & T; so you can use it to cast interface ExtProps{
store?:{foo:boolean};
}
interface IntProps{
store:{foo:boolean};
}
class Me extends React.Component<ExtProps>{
private _:MyProps<IntProps, this> = this as any;
componentDidUpdate(){
this._.props.store.foo; // <-- this._.props is of type IntProps so store is not optional
}
} I don't like having runtime footprints for type workarounds, but I guess that's a choice. |
so there is no workaround for now ? |
work around is marking it optional in props type, and using "!":
`this.props.store!.invokeAction()` or introducing a utility method that
returns the non nulled store for internal use
Op vr 14 jul. 2017 om 14:10 schreef Ostap Chervak <[email protected]
…:
so there is no workaround for now ?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#256 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABvGhGDgn9PAdJdvp55pSHcwH8HVX40Uks5sN1q6gaJpZM4NdcXH>
.
|
Please everyone running TypeScript >= 2.4.1, try this out :) import * as React from "react";
import { inject } from "mobx-react";
import { ObjectOmit } from "typelevel-ts"; // Thanks @gcanti, we <3 you all!
declare module "mobx-react" {
export function inject<D>(
mapStoreToProps: (store: any) => D
): <A extends D>(
component: React.ComponentType<A>
) => React.SFC<ObjectOmit<A, keyof D> & Partial<D>>;
}
const MyComp = ({ label, count }: { label: string; count: number }) =>
<p>
{label} x {count} times!
</p>;
const MyCompConnected = inject((store: any) => ({ count: 1 }))(MyComp);
export const el1 = <MyCompConnected label="hello" />;
export const el2 = <MyCompConnected labels="hello" />; // error! :D
export const el3 = <MyCompConnected label="Hello world!" count={2} />; Thanks to @gcanti for his help and work who made this possible! ;) |
@mattiamanzati I'll try it out first chance I get |
I made this component: import { inject, IReactComponent } from "mobx-react";
function mobxInject<newProps, oldProps extends newProps>(
stores: string[],
wrappedComponent: IReactComponent<oldProps>,
) {
return inject.apply(null, stores)(wrappedComponent) as IReactComponent<newProps>;
}
export default mobxInject;
...
function CompInternal(props: {store:IStore; passedProps: string}) {
}
const Comp = mobxInject<{passedProps: string}, {store:IStore; passedProps: string}>(["store"], CompInternal); |
Also note that an |
Same problem with flowtype. I create my stores as singletons and import them into my components without injecting. |
Can we do something like in the react-redux type definition by explicitly defining the types of InjectedProps and OwnProps? |
The following type definition makes more sense to me, but somehow this fails to be used as a decorator.
export function inject<T, P>(storesToProps: IStoresToProps<T, P>): <TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
target: TFunction
) => TNewFunction & IWrappedComponent<P>; I made the following simple wrapper of export function mobxInject<T, P>(storesToProps: IStoresToProps<T, P>): (
<TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
target: TFunction
) => TNewFunction & IWrappedComponent<P>
) {
return inject(storesToProps) as
<TFunction extends IReactComponent<T & P>, TNewFunction extends IReactComponent<P>>(
target: TFunction
) => TNewFunction & IWrappedComponent<P>;
} |
@mattiamanzati any idea how to convert your definition to work as decorator? |
@RafalFilipek In TypeScript decorators can't change class type definition, so that is impossible unfortunately :) |
Hi, I do not understand why you return: export function inject<T, P>(
storesToProps: IStoresToProps<T, P>
): (<TFunction extends IReactComponent<T | P>>(
target: TFunction
) => TFunction & IWrappedComponent<P>) // WHY TFunction? What makes more sense to me is: export function inject<T, P>(
storesToProps: IStoresToProps<T, P>
): (<TFunction extends IReactComponent<T | P>>(
target: TFunction
) => IReactComponent<P>) // only the container props will be required !! The problem is that if component has compulsory paramaters which are fulfiled by container, using your definition also the container will have those compulsory parameters which is incorrect. The problem of my "fix" is that it does not work with decorator. So, what is the reasoning for returning also the component parameters? And can I somehow fix that to work with decorators? Thanks |
Based on @mattiamanzati's proposal and using a type from @gcanti's typelevel lib, I created a utility type that can be used to get pretty typesafe injection when using the this is the utility type: import { ObjectDiff } from 'typelevel-ts';
export type TypedInject<Stores> = <StoreKeyToInject extends keyof Stores>(
...storeKeysToInject: StoreKeyToInject[]
) => <ExpectedProps extends Pick<Stores, StoreKeyToInject>>(
component: React.ComponentType<ExpectedProps>
) => React.ComponentType<ObjectDiff<ExpectedProps, Pick<Stores, StoreKeyToInject>>>; a short example on how to use it would be like this: const typedInject = inject as TypedInject<Stores>;
const InjectedComponent = typedInject('myStoreName')(MyComponent); The long story of the idea is the following: I'll just make the assumption that in most applications you'll only have a single index.tsx class Store1 {...};
class Store2 {...};
class Store3 {...};
const stores = {
store1: new Store1(),
store2: new Store2(),
store3: new Store3()
};
react.render(<Provider {...stores}> <App /> </Provider>); We know at the design time the name and the shape of the stores that are getting injected, and we can extract the type in typescript via:
Now somewhere deep in your component hierarchy you want to inject one of those stores in a typesafe manner. You can do this in the following way MyComponent.tsx import React from 'react';
import { inject } from 'mobx-react';
import { Store1, Stores } from '../index.ts';
@observer
export class MyComponent extends Component<{ store1: Store1, store3: {incompatibleShape: string} }> {
render() {
<div>{this.props.store1.foo}></div>
}
}
export default typedInject('store1')(MyComponent); // will work and make `store1` an optional prop on the wrapped component
export default typedInject('store2')(MyComponent); // will error because Component does not expect `store2` as prop
export default typedInject('store3')(MyComponent); // will error because store3 from `Stores` is not type compatible with the `store3` prop of the component
export default typedInject('store4')(MyComponent); // will error because store4 does not exist on `Stores` Since I make the assumption that you're only having a single provider and a defined set of stores to inject, you actually can create the E.g. final result may look like: index.tsx import { TypedInject } from '../utils/types';
export class Store1 {...};
export class Store2 {...};
export class Store3 {...};
const stores = {
store1: new Store1(),
store2: new Store2(),
store3: new Store3()
};
const typedInject = inject as TypedInject<typeof stores>;
react.render(<Provider {...stores}> <App /> </Provider>); MyComponent.tsx import React from 'react';
import { Store1, typedInject } from '../index.ts';
@observer
export class MyComponent extends Component<{ store1: Store1 }> {
render() {
<div>{this.props.store1.foo}></div>
}
}
export default typedInject('store1')(MyComponent); This way you're getting relatively typesafe injection E2E without too much boilerplate. P.S.: In a real app you probably shouldn't put all the store definitions, types and typedInject into index.tsx but somewhere else (maybe your |
This gentleman suggests a workaround by extending the main props interface: https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf
|
@geekflyer Thank you for this solution, but I've got |
@shadow-identity do you get this as runtime or typescript compiler error? |
@geekflyer at runtime. |
@FredyC 🤔 You used Am I missing something? In React's source code, it has the following jsdoc tags for /**
* @version experimental
* @see https://reactjs.org/docs/hooks-reference.html#usecontext
*/ |
@JoshMoreno Ok, I see your confusion now. You can use context without hooks, although it's slightly less convenient. It kinda depends on your needs. Personally, I have a single store that has everything so typing is super simple. If you have different needs, you need to tailor it to those. import { createContext } from 'react'
export const { Consumer } = createContext<TStores>(createStores())
// in some other file
export function MyComponent() {
return <Consumer>{stores => {
// do whatever you want here, stores would be fully typed to specified TStores
return stores.friend.name
}}</Consumer>
} You can find everything you need in the React docs. I am sorry, but I don't want to spend time writing up something more elaborate without know what you need. |
My version has become as below and is TypeScript 3.1.6 compliant: declare module "mobx-react" {
export function inject<D extends object>(
mapStoreToProps: (store: IRootStore) => D
): <A extends D>(
component: React.ComponentType<A> | React.SFC<A>
) => React.SFC<Omit<A, keyof D> & Partial<D>> & IWrappedComponent<A>;
} Omit is defined as type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; |
We can get type safety on both the injected props and the "remainder" passed-in props by using the non-decorator syntax for It doesn't feel great but since using the non-null assertion operator ( import { observable } from 'mobx'
import { inject, observer, Provider } from 'mobx-react'
import * as React from 'react'
class PersonStore {
@observable public name: string = 'Rick Sanchez'
}
interface AllStores {
personStore: PersonStore
}
interface InjectedProps {
name: string
}
interface PassedProps {
age: number
}
@observer
class ExamplePresentational extends React.Component<PassedProps & InjectedProps> {
public render () {
const { age, name } = this.props
return (
<div>
<h3>Name: {name}</h3>
<h4>Age: {age}</h4>
</div>
)
}
}
const ExampleContainer = inject<AllStores, PassedProps, InjectedProps, {}>(({ personStore }: AllStores) => ({
name: personStore.name,
}))(ExamplePresentational as unknown as React.ComponentClass<PassedProps>)
class Sandbox extends React.Component {
public render () {
return (
<Provider personStore={new PersonStore()}>
<ExampleContainer age={70} />
</Provider>
)
}
} |
@kylecorbelli Why would you be shooting yourself in the foot with such hassle when it's much easier to just use Context API? Unless you are with older React and cannot upgrade. In that case nevermind. DIY |
I think there's a simpler way:
|
This solution works great for me: type AppProps = {
myMobxStore?: IMyMobxStore; // injected
title: string; // passed as <App title="my title">
};
@inject("myMobxStore")
@observer
class App extends Component<AppProps> {
render() {
const { myMobxStore, title }: AppProps = this.props;
if (!myMobxStore) return null;
const language = myMobxStore.language;
return (
<div>
<h1>{title}</h1>
</div>
);
}
} You can savely return |
I ended up using this approach, based on that heavily-mentioned workaround: import { Component } from "react";
import { inject } from "mobx-react";
import { RootStore } from "../stores"; // App's root store as a singleton
export class ConnectedComponent<T, S, X = {}> extends Component<T, X> {
public get stores() {
return (this.props as any) as S;
}
}
export const connect = (...args: Array<keyof RootStore>) => inject(...args); Example: import * as React from "react";
import { ConnectedComponent, connect } from "../utils";
import { UserStore } from "../stores";
interface IProps {
onClose: () => void;
}
interface IStores {
userStore: UserStore;
}
@connect("userStore") // Autocomplete works here ✔️ ❤️
export class UserProfile extends ConnectedComponent<IProps, IStores> {
public render() {
return (
<UserDetails
data={this.stores.userStore.user} // Also here ✔️
onClose={this.props.onClose} // and here ✔️ :)
/>
)
}
} This way, stores and regular props are properly isolated. It is also possible to set native |
Full disclosure, I've just been using a little hand-rolled render-prop component with the Context API: import { Observer } from 'mobx-react'
import { Store } from '../store'
import { store } from '../store/initial-store'
const StoreContext = React.createContext(store)
interface Props {
children(store: Store): JSX.Element | null
}
export const WithStore: React.FunctionComponent<Props> = ({ children }) =>
<StoreContext.Consumer>
{store => (
<Observer>
{() => children(store)}
</Observer>
)}
</StoreContext.Consumer> so that later I can write something along the lines of: import { WithStore } from '../../contexts/WithStore'
import { AddBook as AddBookPresentational } from './AddBook'
export const AddBook: React.FunctionComponent = () =>
<WithStore>
{store => (
<AddBookPresentational
addBook={store.booksStore.addBook}
/>
)}
</WithStore> 100% type-safe, no type casting with I'd go for a similar approach with hooks if your project supports at least |
Another HOC solution, based on the one posted above by @onlyann: // Somewhere else: create a type with all of your stores. "bootstrap" here is a function that creates my stores.
export type StoresType = ReturnType<typeof bootstrap>;
export type Subtract<T, K> = Omit<T, keyof K>;
export const withStores = <TStoreProps extends keyof StoresType>(...stores: TStoreProps[]) =>
<TProps extends Pick<StoresType, TStoreProps>>(component: React.ComponentType<TProps>) => {
return (inject(...stores)(component) as any) as
React.FC<Subtract<TProps, Pick<StoresType, TStoreProps>> &
Partial<Pick<StoresType, TStoreProps>>> &
IWrappedComponent<TProps>;
}; Usage: @observer
class SomeComponent extends Component<{customerStore: CustomerStore}> {
...
}
export default withStores('customerStore')(SomeComponent)); You get nice typings and IDE autocomplete, but still use the string version of inject to reduce verbosity. |
Nice. Say that {
customerStore: CustomerStore;
authStore: AuthStore
} Does it identify that the following is incorrect: export default withStores('authStore')(SomeComponent)); |
@onlyann Why is that example incorrect? You are requesting one of your stores, there shouldn't be any problem. If you would've misspelled 'authStore', typescript would've complained. |
In the example above, |
@onlyann oh, for some reason I didn't realize you were referring to my
|
We use custom You can see an example of these typings here: https://github.com/appulate/strict-mobx-react-inject-typings-example/blob/master/mobx-react.d.ts. |
Coming in late to the game but I was able to solve this and still use inject as a decorator with all the regular typings by simply splicing in some defaultProps on the components that inject. Example:
This allows me to have this.props.mainStore as not optional, but also doesn't require me to explicitly provide it when using SomeComp. Hope this helps someone! |
Expanding on @ivanslf suggestion: // /stores/index.ts
import { inject, IStoresToProps } from 'mobx-react'
import { Component } from 'react'
import UserStore from './UserStore' // importing stores
import AuthStore from './AuthStore' // importing stores
export interface IStores {
user: UserStore,
auth: AuthStore
}
export class ConnectedComponent<P, I, S = {}> extends Component<P, S> {
public get injects() {
return (this.props as any) as I
}
}
export function connect(...storeNames: Array<keyof IStores>): ReturnType<typeof inject>
export function connect<P, C, I>(
mapStoresToInjected: IStoresToProps<IStores, P, C, I>,
): ReturnType<typeof inject>
export function connect(): ReturnType<typeof inject> {
return inject(...arguments)
} This now makes our new inject (named import { observer } from 'mobx-react'
import React from 'react'
import { connect, ConnectedComponent, IStores } from 'stores'
@connect('user')
class String extends ConnectedComponent<{}, { user: IStores['user'] }> { // generics order: Props, Injected, State
render() {
const userStore = this.injects.user
return <div>Store injected!</div>
}
}
// or
const mapStoresToInjects = (stores: IStores) => ({
progress: stores.user.userProgress,
isLoggedIn: !!stores.user.data,
})
@connect(mapStoresToInjects)
class Mapped extends ConnectedComponent<{}, ReturnType<typeof mapStoresToInjects>> {
render() {
const { progress, isLoggedIn } = this.injects
return <div>Mapped stores injected!</div>
}
} |
why not just put the store type as an optional field on props where it belongs? never had an issue with this .. |
See mobxjs/mobx#1974 |
Needs non-null assertions everywhere when using TS in strict mode. E.g.
this.props.store!.x. (it is still what I typically do though)
…On Thu, Oct 17, 2019 at 6:04 AM Fredrik Wollsén ***@***.***> wrote:
why not just put the store type as an optional field on props where it
belongs? never had an issue with this ..
See mobxjs/mobx#1974 <mobxjs/mobx#1974>
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#256?email_source=notifications&email_token=AAN4NBG7WA6BHFIGGBBYMQTQO7W67A5CNFSM4DLVYXD2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBOZHSA#issuecomment-543003592>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBBRZVZ3XKTBJ27HUHTQO7W67ANCNFSM4DLVYXDQ>
.
|
I'm very fond of the approach suggested by @dested. defaultProps distinguishes regular props from ones that were magically received from a wrapper, whether it's MobX Since it may be not easy to distinguish between default and injected values, I use it in simplistic form that results in undefined values at runtime in case the injection went wrong: export type MainStoreProps = {mainStore: MainStore};
interface Props extends MainStoreProps{}
@inject(MainStoreName)
@observer
export class SomeComp extends Component<Props> {
static defaultProps = {} as MainStoreProps;
} |
interface Props {
// component props
}
interface StoreProps {
// injected props
}
@inject('storeName')
@observer
export class MyCoponentContainer extends React.Component<Props> {
public get props() {
return this.props as Props & StoreProps;
} |
That looks a lot like an indefinite loop
…On Wed, Apr 1, 2020 at 1:05 PM Dima Panov ***@***.***> wrote:
interface Props {
// component props
}
interface StoreProps {
// injected props
}
@Inject('storeName')
@observerexport class MyCoponentContainer extends React.Component<Props> {
public get props() {
return this.props as Props & StoreProps;
}
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#256 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBGB7HXWXFE3JT7JQ3DRKMUZ7ANCNFSM4DLVYXDQ>
.
|
Not a mobx- or TypeScript-specific solution but I have a workaround for this injection issue, for those who are interested: In this structure: <Steps>
<Step title="My First Step" component={Step1} />
<Step title="My Second Step" component={Step2} />
<Step title="My Third Step" component={Step3} />
</Steps> My I am basically
Props of my So {React.Children.map(children, (child, order) => (
<StepContext.Provider value={order + 1}>{child}</StepContext.Provider>
))} |
I've looked at
https://github.com/mobxjs/mobx-react#strongly-typing-inject
but am not sure if there is a better way. I generally pick out a lot of things to inject. Imagine
But if I render this component from a parent component, then I can't render it without marking each one of the stores or props as optional in IMyComponentProps or pass in values for each of those props. This is documented but makes it very difficult to actually use the props in the component.
Basically anytime I use something from storeA, storeB, or somethingOnStoreD - I first have to check for the presence of those or else typescript will complain that it is potentially undefined.
I think part of the problem is typescript currently doesn't allow class mutations from decorators. But even when I use inject as a function, the tsd file specifies inject returns the original target which expects the props declared in IMyComponentProps
export function inject<T, P>(storesToProps : IStoresToProps<T, P>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (TFunction & IWrappedComponent<T>)); // decorator
If I were to change it the tsd to
export function inject<T, P>(storesToProps : IStoresToProps<T, P>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (IReactComponent<P> & IWrappedComponent<T>)); // decorator
and inject with
This allows me to use the function to return a component that no longer requires all the props that will be inserted with injection. It also will validate that the inject function is returning the props that I am expecting and also allows MyComponent to use props from injection directly and force checks on props from parent. The problem is this declaration will no longer allow inject to be used as a decorator since its no longer clear that inject returns a signature compatible with the original constructor. I kind of wish this functionality somehow existed because I could be willing to forego decorators to have a better type-checking experience but not sure if its possible.
Sorry this was a bit long, do you know of a better way to solve these problems?
Thanks!
The text was updated successfully, but these errors were encountered: