Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help me understand 'Types.RootState' #85

Closed
kilgarenone opened this issue Jul 25, 2018 · 16 comments
Closed

Help me understand 'Types.RootState' #85

kilgarenone opened this issue Jul 25, 2018 · 16 comments

Comments

@kilgarenone
Copy link

Sorry if this was a dump question- In this connected component, I'm confused about the Types and Types.RootState. Is the Types a npm package cuz it doesn't seem to be in the package.json?

@piotrwitek
Copy link
Owner

This is a global scope type annotation, if you'd open project and search all references everything should be clear

@carpben
Copy link

carpben commented Feb 3, 2019

Thanks for this guide.
I find import Types from "Types" very confusing. According to the syntax Types refers to a library named Types, but after checking in NPM it's not the case. Shouldn't it be import Types from "./Types".

There is still the challenge of how and where to create the "RootState" type. There should be a State type defined in each of the separate reducers. I guess file containing the combine reducers is the appropriate place for the RootState type. Makes sense?

@piotrwitek
Copy link
Owner

@carpben I'm using Ambient Modules technique, you can learn more here:
https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules

And also here: #97

@chawax
Copy link
Contributor

chawax commented Feb 3, 2019

I use typesafe-actions library to achieve this. It makes it easy to create a RootState type that you can export from the same file where you combine reducers.

For example :

import { combineReducers } from 'redux'
import { StateType } from 'typesafe-actions'
import { reducer as formReducer } from 'redux-form'
import { reducer as authReducer } from './AuthRedux'
import { reducer as navReducer } from './NavigationRedux'

export const reducers = combineReducers({
  auth: authReducer,
  form: formReducer,
  nav: navReducer,
})

export type RootState = StateType<typeof reducers>

Then it mapStateToProps for example :

const mapStateToProps = (state: RootState) => ({
  isAuthenticated: state.auth.isAuthenticated,
})

@piotrwitek
Copy link
Owner

@chawax I used that in the past but it's not an optimal solution and introduces all kind of issues when scaling your codebase, primarily because then in your components you are adding hard-dependency on this module like this:

import {RootState} from '../../rootReducer'; <= hard dependency

It will add all of your reducers and their dependencies as a dependency to your components and you'll have to mock tons of unrelated stuff when trying to test in isolation. That means you have tight coupling in your codebase and this is considered a code smell.

With my approach (the one that is using purely ambient type definition to define global scope types using d.ts files #97), you're not be impacted by any of that issues.

@piotrwitek
Copy link
Owner

piotrwitek commented Feb 3, 2019

@chawax I just realized there is more to my argument above that needs to be added here:

  1. In theory, I think you could prevent a hard-dependency issue by using a type level import syntax, but I haven't confirmed that so you have to check yourself.
type RootState = import("../../rootReducer'").RootState;

// but it's still more cumbersome to use and more error-prone
// when using with other global types, consider comparing it to this:
import { RootState, Services, RootAction, TodoModel, ... } from '@Types';
  1. A more important argument here is the scalability of that solution. You can very easily extend and remove any global level type because of the inversion of control principle.
    Also your @Types imports are consistent in every module across the entire application without a worry where does it come (you can always jump to it with F12). And all of the above reasons will make your application easier to extend and to maintain in the long run.

@chawax
Copy link
Contributor

chawax commented Feb 3, 2019

Something like that ?

types/index.d.ts file :

import { IState as AuthState } from '../redux/AuthRedux'

export interface RootState {
  readonly auth: AuthState;
}

with AuthState defined in my AuthRedux file

Then :

import { RootState } from '../types';

const mapStateToProps = (state: RootState) => ({
  isAuthenticated: state.auth.isAuthenticated,
})

@piotrwitek
Copy link
Owner

piotrwitek commented Feb 3, 2019

Yes correct, but notice I prefer to add "Ambient Modules" technique (linked above) because that gives me absolute import capability, without paths mapping and aliasing configuration concerns.

I don't really like relative imports for global stuff because it's hard to maintain.

@chawax
Copy link
Contributor

chawax commented Feb 3, 2019

I don'k now ambient modules (a lot of things to learn about TS yet !). Do you have any example how you declare such modules to have absolute imports ?

@piotrwitek
Copy link
Owner

Please check /playground project here in the repository, you can find it in the store folder

@chawax
Copy link
Contributor

chawax commented Feb 3, 2019

Great ! Thanks a lot !

@carpben
Copy link

carpben commented Feb 3, 2019

@piotrwitek ,

  1. What do u mean by hard dependency? Is it that soft dependency a simple form of a type, and hard dependency the same type but "mapped", or interpreted from another type? I guess the main problem with such types is that they are more difficult to read. Would you consider
const reducers = combineReducers({reducer1, reducer2})
type RootState = ReturnType<reducers>

When RootState is imported would you consider it to be a hard import?

  1. regarding 'Ambient Modules technique' I read the resources you suggested. I've noticed you defined type of Types here: https://github.com/piotrwitek/react-redux-typescript-guide/blob/master/playground/typings/modules.d.ts . But how does it know (or where is it defined) which object to import?

@piotrwitek
Copy link
Owner

Ad1) By hard dependency I mean that you have a runtime dependency that you have to mock it during testing in isolation. I agree the example I have provided is not the best one, but you can imagine real-world situation (commonly happens with the ducks-pattern), when you're importing the type (State, ActionType) and some other runtime export (selectors, actionCreators) from the same file, then it'll be a problem.
But with the clear separation of type imports (*.d.ts files) and runtime imports from regular modules the intention is more clear and it's easier to find the problem in such case.

Ad2) I don't understand your question. An ambient definition doesn't import anything, you simply extend its definition across multiple files, that's how it works.

@carpben
Copy link

carpben commented Feb 14, 2019

  1. So In other words, An import
import {RootState} from '../../rootReducer'; <= hard dependency

Though it just imports a type, it will make rootReducer a dependency of the component file, which will create a problem when trying to test the component independently?

@piotrwitek
Copy link
Owner

No no, when only importing type it is removed on the compilation step so it's fine, but you're opening a gate to import some other runtime exports (like selectors, actionCreators, type constants etc.) from the same file, then it's starting to be complicated and problematic

@carpben
Copy link

carpben commented Feb 24, 2019

Thanks @piotrwitek. I think this technique makes sense.
In other words, you don't want to import stuff from a reducer file, into a component file (or connected component file). On the other hand, you do want to type your state, based on the implementation of the reducer.

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

No branches or pull requests

4 participants