by Juraj Husár
Let's talk a little bit about one really interesting language based on JavaScript - it's TypeScript.
Type system for JavaScript
Powerful type system - better than Java has.
function getParam(parameter: number | string): string {
if (typeof parameter === 'number') {
return parameter; // error - Type 'number' is not assignable to return type 'string'
}
return parameter; // ok - numberOrStringParam: string
}
If you are not convinced with TypeScript, nt sure why you would not be, there is still alternative - flow
Main advantage of Flow should be more soundless checking without need to specify types. But TypeScript also got similar features and became relatively clever in type inference.
See nice table of differences: https://github.com/niieani/typescript-vs-flowtype
Real world experience is that TypeScript is more mature than Flow (as of 2017, update: still in 2018). There is better tooling and more types definitions for packages.
Note that you can benefit from TypeScript setup even if you are working with plain JavaScript. TypeScript compiler itself is much more clever than plain JS alternatives.
# Install typescript to project and save into package.json
npm i typescript --save-dev
# Create tsconfig.json typescript configuration file
tsc --init
tsconfig.json contains all settings for typescript compiler. You can compile also .js
files with option "allowJs": true
Add TypeScript webpack loader (https://github.com/TypeStrong/ts-loader):
Simplest TypeScript webpack webpack.config.js:
module.exports = {
entry: './main.ts',
output: {
filename: 'bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
loaders: [
{ test: /\.tsx?$/, loader: 'ts-loader' }
]
}
}
Alternatively you can accelerate your development start with React CLI
. Sadly, there is no official support for TypeScript, but there is maintained fork, so that you can generate new application with it.
See https://github.com/wmonk/create-react-app-typescript
npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start
Redux contains it's own types inside library, so these types will be installed with Redux itself - https://github.com/reactjs/redux/blob/master/index.d.ts
To gain great benefits of TypeScript, we need to make sure, we do not loose any type after connect
in Container
, or when working with state in Reducers
.
Please note that there might be different Redux setups and this is just one variation, which worked well for me.
1) Actions
Configure all actions in one single object, place it in actions/types.ts
:
export type Action =
| { type: '@@router/LOCATION_CHANGE'; payload: { pathname: string; search: string; hash: string } }
| { type: 'EXPAND_MENU'; menuId: number }
| { type: 'COLLAPSE_ALL_MENUS' }
| ...
Now everywhere where we will be defining actions, we will expect this object contain all possible actions.
Specific actions eg. in actions/menu.ts
would looks like following:
import { Action } from './types';
export const expandMenu = (menuId): Action => ({
type: 'EXPAND_MENU'
});
export const collapseAllMenus = (): Action => ({
type: 'COLLAPSE_ALL_MENUS'
});
2) Reducers (with fully typed store state)
Reducers will also contain whole State
typed object.
import { Action } from '../actions/types';
export interface IExpandedMenus {
[id: string]: boolean;
}
export const expandedMenus = (state: IExpandedMenus = {}, action: Action): IExpandedMenus => {
switch(action.type) {
case 'EXPAND_MENU':
return {
...state,
[action.menuId]: true
}
case 'COLLAPSE_ALL_MENUS':
return {};
default:
return state;
}
};
Now, we have connected Action
and Reducer
, try to make some changes to global action type, you should directly see that reducer need to be updated too. This will eliminate many possible integration and refactoring issues.
What is also important is to create global store State
and Dispatch
.
export interface IState {
expandedMenus: IExpandedMenus;
router: ReactRouterState;
// All other reducer types/ type combined trees
}
export type IDispatch = (action: Action) => void;
3) Containers (with fully typed connects)
After we have actions and reducer, we are missing just one thing and it is connection to real React component.
This is done with following code:
import { connect } from 'react-redux';
import { IState, IDispatch } from '../reducers/types';
import { expandMenu, collapseAllMenus } from '../actions/menu';
import { IOwnProps, IStateProps } from Component;
const mapStateToProps = (state: IState, ownProps: IOwnProps): IStateProps => ({
expandedMap: state.expandedMenus
});
const mapDispatchToProps = (dispatch: IDispatch): IDispatchProps => ({
onExpandMenu (id: string) {
dispatch(expandMenu (id));
},
onCollapseAll() {
dispatch(collapseAllMenus());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Component);
Component needs to have defined and exported required interfaces.
And that's all.
Not so simple, but when integrated properly, you will gain great benefits of bulletproof codebase and you can focus on building functionality instead of finding bugs!