Skip to content
This repository has been archived by the owner on Dec 31, 2020. It is now read-only.

[Question] How to get typesafe injection #256

Closed
noveyak opened this issue May 17, 2017 · 83 comments
Closed

[Question] How to get typesafe injection #256

noveyak opened this issue May 17, 2017 · 83 comments

Comments

@noveyak
Copy link

noveyak commented May 17, 2017

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

@inject((stores) => {
  return {
    storeA: stores.storeA,
    storeB: stores.storeB,
    storeC: stores.storeC,
    somethingOnStoreD: stores.storeD.something
})
MyComponent extends React.Component<IMyComponentProps>

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

interface IPropsFromStore {
  storeA: storeA;
  storeB: storeB;
  ...
}
IPropsFromParent {
}
inject<IPropsFromStore, IPropsFromParent>((stores) => {
  return {
    storeA: stores.storeA,
    storeB: stores.storeB,
    storeC: stores.storeC,
    somethingOnStoreD: stores.storeD.something
})(
MyComponent extends React.Component<IPropsFromStore & IPropsFromParent>)

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!

@jamiewinder
Copy link
Member

jamiewinder commented May 18, 2017

Not sure if you're aware, but you can tell TypeScript that you know something is defined by using the following syntax (note the ! on the property access):

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.

@mweststrate
Copy link
Member

The current declaration of React components don't allow this behavior. The only way to properly fix it is to define React.Component with three generic params <PApi, PInside, S>, so that the PApi could define fields as being optional, while PInside is used inside the components, and can mark the same fields as non-optional.

E.g.: MyComponen extends React.Component<{ store?: StoreType }, { store: StoreType }, {}>. That is imho the only proper way to fix this; make a PR on the React component typings. Since in the latest TS versions Generics can have default arguments, this doesn't need to be as breaking as it would have been in the past, if it is possible to express: ReactComponent<P={}, S={}, PInner = P>.

Hope that makes sense. Not much can be done about it from mobx-react side of things.

@dvdzkwsk
Copy link

dvdzkwsk commented Jun 25, 2017

Not sure if it's of any help, but I just started picking up MobX and decided to go with a custom inject/observer wrapper to achieve this. The type definitions could arguably be a bit stronger, but for an initial pass it seems to work quite well:

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, MyComponent does not have to treat the props as if they were optional, and any component rendering the connected (injected) MyComponent does not have to supply any of its props. Clearly what we'd really want is for the connected component to expose only the subset of props that weren't selected via inject, but my limited TypeScript skills have hindered me from accomplishing that thus far.

@DeDuckProject
Copy link

Any news about this?
@amir-arad and I tried a different implementation:

interface MyComponentInternalProps {
    myStore: MyStore;
}

export interface MyComponentExternalProps {
    myStore?: MyStore;
}

export interface MyComponentState {

}

@inject(STORE)
@observer
class MyComponentInternal extends React.Component<MyComponentInternalProps, MyComponentState> {

    render() {
        return (
            <div>
                Hi
            </div>
        );
    }
}

class JSXInterface extends React.Component<MyComponentExternalProps, MyComponentState>{}
export const MyComponent = MyComponentInternal as typeof JSXInterface;

It works, but its messy.

@beshanoe
Copy link

beshanoe commented Jul 5, 2017

There is an article on this topic, yet another way to deal with this https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf
But for me the question is whether it is a good practice to use a bang! operator like
this.props.todoStore!.fetchData() or to use the approach from article like this.injected.todoStore.fetchData()
Which of these is more readable and maintainable, what do you think guys?

@amir-arad
Copy link

I'd add that the bang nullifies any existing type inference.
see: microsoft/TypeScript#16945

@mweststrate
Copy link
Member

mweststrate commented Jul 6, 2017

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:

React.Component<PropsExternal, PropsInternal, State> where PropsInternal specifies which properties are available inside the component's methods. The PropsExternal would specificy the interface to the outside world.

In that case you could have a component with the following signature: class Component<{ todoStore?: TodoStore }, { todoStore: TodoStore }, {}> { ...}

Probably the inject signature could even be used to leverage Partials and construct the external properties from the internal ones. Nope, type inference starts with the fn, not the components

I am wondering btw whether

export function inject<T, P>(storesToProps : IStoresToProps<T, Partial<P>>): (<TFunction extends IReactComponent<T | P>>(target: TFunction) => (TFunction & IWrappedComponent<T>)); // decorator
Could help (adding partial to this def)

@amir-arad
Copy link

amir-arad commented Jul 6, 2017

I think the root of the problem is in how they implemented JSX support. currently it just uses the props field's type for type checking.
we need a marker type like JSXAble<T>, so every class that implements it can be used as JSX with arguments of type T. However I don't know how it will play out with duck-typings. maybe using a hidden symbol field with type T.

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 this for the methods like so:

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.

@KatSick
Copy link

KatSick commented Jul 14, 2017

so there is no workaround for now ?

@mweststrate
Copy link
Member

mweststrate commented Jul 14, 2017 via email

@mattiamanzati
Copy link

mattiamanzati commented Jul 14, 2017

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! ;)

@amir-arad
Copy link

@mattiamanzati I'll try it out first chance I get

@Bnaya
Copy link
Member

Bnaya commented Jul 25, 2017

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);

@mweststrate
Copy link
Member

Also note that an @inject decorator for fields could quite nicely fit the typing problem, like done here: https://github.com/ascoders/dob-react

@kuuup-at-work
Copy link

Same problem with flowtype.

I create my stores as singletons and import them into my components without injecting.

@zhahaoyu
Copy link

Can we do something like in the react-redux type definition by explicitly defining the types of InjectedProps and OwnProps?

@zhahaoyu
Copy link

The following type definition makes more sense to me, but somehow this fails to be used as a decorator.
Reasons:

  1. The wrapped component should accept both the injected props T as well as props P passed by parent, therefore I changed | to &
  2. The new component generated from the HOC should only expose the props P, and T is no longer visible to the outside because they have been injected by the mapper function, so instead of using TFunction, we use TNewFunction
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 inject which seems to do the work.

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>;
}

@RafalFilipek
Copy link
Contributor

@mattiamanzati any idea how to convert your definition to work as decorator?
Thank you!

@mattiamanzati
Copy link

@RafalFilipek In TypeScript decorators can't change class type definition, so that is impossible unfortunately :)

@tomitrescak
Copy link

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

@geekflyer
Copy link

geekflyer commented Oct 9, 2017

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 inject(<storeName>)(MyComponent) variant.
Using this utility type, the type system will mark the injected property optional in the wrapped component, but also it will make sure that the component you're injecting to has a property which is type compatible with the injected store.

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 <Provider ... /> at the entry point of your app.
Now let's suppose you're injecting 3 stores into the provider.
E.g.

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:

export type Stores = typeof stores; we can just add that type to the index.tsx and then import it from anywhere.

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 typedInject variable in a central file (like index.tsx) and just import it from anywhere without always casting the untyped inject in multiple places.

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 stores directory).

@ddbradshaw
Copy link

This gentleman suggests a workaround by extending the main props interface: https://medium.com/@prashaantt/strongly-typing-injected-react-props-635a6828acaf

interface MyComponentProps {
  name: string;
  countryCode?: string;
}

interface InjectedProps extends MyComponentProps {
  userStore: UserStore;
  router: InjectedRouter;
}
@inject("userStore")
@withRouter
@observer
class MyComponent extends React.Component<MyComponentProps, {}> {
  get injected() {
    return this.props as InjectedProps;
  }

  render() {
    const { name, countryCode } = this.props;
    const { userStore, router } = this.injected;
    ...
  }
}

@shadow-identity
Copy link

@geekflyer Thank you for this solution, but I've got
TypeError: Object(...) is not a function at export default typedInject('journal')(CalendarButton);
(full component here https://github.com/shadow-identity/therapy_journal/blob/e14f731a0a50b7b64efd1e6ab16e29b46181a7d9/src/CalendarButton.tsx)
What's wrong here?

@geekflyer
Copy link

geekflyer commented Oct 20, 2017

@shadow-identity do you get this as runtime or typescript compiler error?

@shadow-identity
Copy link

@geekflyer at runtime.

@JoshMoreno
Copy link

@FredyC 🤔 You used useContext in your example. I don't see an alternate version that isn't using a new "React Hook". useContext is not part of the new context api of 16.3, but createContext is.

Am I missing something?

In React's source code, it has the following jsdoc tags for useContext

/**
 * @version experimental
 * @see https://reactjs.org/docs/hooks-reference.html#usecontext
 */

@danielkcz
Copy link
Contributor

danielkcz commented Nov 20, 2018

@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.

@onlyann
Copy link

onlyann commented Nov 21, 2018

@JoshMoreno

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>>;

@kylecorbelli
Copy link

kylecorbelli commented Dec 25, 2018

We can get type safety on both the injected props and the "remainder" passed-in props by using the non-decorator syntax for inject and forcing a type assertion on the wrapped component.

It doesn't feel great but since using the non-null assertion operator (!) is also kind of a hack I figured I'd offer up an alternative hack. 😁

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>
        )
    }
}

@danielkcz
Copy link
Contributor

@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 inject if you have embraced React Hooks.

@JulianG
Copy link

JulianG commented Feb 2, 2019

I think there's a simpler way:
https://gist.github.com/JulianG/18af9b9ff582764a87639d61d4587da1#a-slightly-better-solution

interface InjectedProps {
  bananaStore: BananaStore; // 👍 no question mark here, and no interface inheritance
}

interface BananaProps {
  label: string;
}

@inject('bananaStore')
@observer
class BananaComponent extends Component<BananaProps> {

  get injected(): InjectedProps {
    return this.props as BananaProps & InjectedProps;
  }

  render() {
    const bananas = this.injected.bananaStore.bananas; // 👍 no exclamation mark here
    return <p>{this.props.label}:{bananas}</p>
  }

}

@JiiB
Copy link

JiiB commented Feb 5, 2019

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 null because you can be sure that myMobxStore is always present as long as you inject it

@ivanfilhoz
Copy link

ivanfilhoz commented Feb 25, 2019

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 IState interface as the third parameter -- this is obviously discouraged, though.

@kylecorbelli
Copy link

kylecorbelli commented Feb 25, 2019

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 as any, no non-null assertions with !, no null checks with if (this.props.store) {...}.

I'd go for a similar approach with hooks if your project supports at least v16.8.0.

@orzarchi
Copy link

orzarchi commented Apr 3, 2019

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.

@onlyann
Copy link

onlyann commented Apr 4, 2019

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 StoresType looks like:

{
  customerStore: CustomerStore;
  authStore: AuthStore
}

Does it identify that the following is incorrect:

export default withStores('authStore')(SomeComponent));

@orzarchi
Copy link

orzarchi commented Apr 4, 2019

@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.

@onlyann
Copy link

onlyann commented Apr 4, 2019

In the example above, SomeComponent only has customerStore prop. From what I see (and I could be mistaken as I haven’t tried it), Typescript won’t complain but SomeComponent will likely fail at runtime given its customerStore prop in undefined.
It’d be great to be able to catch it early.

@orzarchi
Copy link

orzarchi commented Apr 4, 2019

@onlyann oh, for some reason I didn't realize you were referring to my SomeComponent example.
So the answer is yes, that will be marked by typescript as incorrect.
edit: let's separate to two cases:

  1. Component expects customerStore as props, and authStore is injected: will be caught.
  2. Component expects customerStore and authStore as props, and only authStore is injected - will unfortunately not be caught.

@vkrol
Copy link
Contributor

vkrol commented May 27, 2019

We use custom mobx-react/inject typings based on react-redux/connect typings from @types/react-redux package. We do not have to use optional properties or custom inject wrappers. Everything works like a charm. The only significant downside: inject cannot be used as decorator, but it doesn't bother us.

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.
You can see an example of usage here: https://github.com/appulate/strict-mobx-react-inject-typings-example/blob/master/index.tsx.
Link to the repository that you can clone and play with: https://github.com/appulate/strict-mobx-react-inject-typings-example.

@dested
Copy link

dested commented Jun 9, 2019

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:

export type MainStoreProps = {mainStore: MainStore};
export const mainStoreDefaultProps = {mainStore: (null as unknown) as MainStore};

interface Props extends MainStoreProps{}

@inject(MainStoreName)
@observer
export class SomeComp extends Component<Props> {
  static defaultProps = mainStoreDefaultProps;
}

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!

@shilangyu
Copy link

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 connect) strongly typed. We get autocomplete when injecting with a string and when mapping stores to props. Example usage:

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>
	}
}

@coglite
Copy link

coglite commented Oct 17, 2019

why not just put the store type as an optional field on props where it belongs? never had an issue with this ..

@motin
Copy link

motin commented Oct 17, 2019

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

@mweststrate
Copy link
Member

mweststrate commented Oct 17, 2019 via email

@bisubus
Copy link

bisubus commented Dec 21, 2019

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 inject, Redux connect or else.

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;
}

@DmtrPn
Copy link

DmtrPn commented Apr 1, 2020

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;
    }

@mweststrate
Copy link
Member

mweststrate commented Apr 1, 2020 via email

@sametweb
Copy link

sametweb commented Nov 2, 2020

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 Steps component calculates an order number for each Step component to pass down to its specific components (Step1, Step2, etc.)

I am basically

  1. mapping over the children
  2. wrapping each of them in a Context.Provider
  3. passing my order number as value.

Props of my Step component are not affected, auto-complete for props object doesn't get messed up, and data is still available to each and every one of them.

So Steps returns this:

{React.Children.map(children, (child, order) => (
    <StepContext.Provider value={order + 1}>{child}</StepContext.Provider>
))}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests