This guide is NOT about "How to write type declarations for every possible variable and expression to have 100% type covered code and waste a lot of time".
This guide is about "How to write type declarations to only the minimum necessary amount of JavaScript code and still get all the benefits of Static Typing".
found it usefull, want some more? give it a ⭐
- extend HOC section with more advanced examples #5
- investigate typing patterns for generic component children #7
This guide is aimed to use --strict
flag of TypeScript compiler to provide the best static-typing experience.
Benefits of this setup and static-typing in general include:
- when making changes in your code, precise insight of the impact on the entire codebase (by showing all the references in the codebase for any given piece of code)
- when implementing new features compiler validate all props passed to components or injected by connect from redux store, validation of action creator params, payload objects structure and state/action objects passed to a reducer - showing all possible JavaScript errors)
Additionally static-typing will make processes of improving your codebase and refactoring much easier and give you a confidence that you will not break your production code.
- Complete type safety with strict null checking, without failing to
any
type - Minimize amount of manually writing type declarations by leveraging Type Inference
- Reduce redux boilerplate code with simple utility functions using Generics and Advanced Types features
Code examples are generated from the source code in playground
folder. They are tested with TypeScript compiler with the most recent version of TypeScript and relevant type definitions (like @types/react
or @types/react-redux
) to ensure they are still working with recent definitions.
Moreover playground is created is such way, that you can easily clone repository, install npm
dependencies and play around with all the examples from this guide in real project environment without any extra setup.
README.md
is auto-generated usinggenerator script
- to make changes please editmarkdown
files located in/docs
folder- Source Code snippets are injected with
generator script
- to make changes please edit their original sources located in playground folder (include directives are used inmarkdown
files located in/docs
folder e.g.::example='../../playground/src/components/...
) - Before making PR please re-generate final
README.md
using command:
sh generate.sh
// or
node generator/bin/generate-readme.js
npm i -D @types/react @types/react-dom @types/react-redux
"react" - @types/react
"react-dom" - @types/react-dom
"redux" - (included in npm package)*
"react-redux" - @types/react-redux
*There are improved redux types on a
next
branch in the official redux github repo, use below instructions to add it to your project:
- in
package.json > devDependencies
add:
"redux-next": "reactjs/redux#next"
- in
tsconfig.json > compilerOptions > paths
add:
"redux": ["node_modules/redux-next"]
- convenient alias:
React.SFC<Props> === React.StatelessComponent<Props>
import * as React from 'react';
export interface SFCCounterProps {
label: string,
count: number,
onIncrement: () => any,
}
export const SFCCounter: React.SFC<SFCCounterProps> = (props) => {
const { label, count, onIncrement } = props;
const handleIncrement = () => { onIncrement(); };
return (
<div>
{label}: {count}
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
};
SHOW USAGE
import * as React from 'react';
import { SFCCounter } from '@src/components';
let count = 0;
const incrementCount = () => count++;
export default () => (
<SFCCounter
label={'SFCCounter'}
count={count}
onIncrement={incrementCount}
/>
);
import * as React from 'react';
export interface SFCSpreadAttributesProps {
className?: string,
style?: React.CSSProperties,
}
export const SFCSpreadAttributes: React.SFC<SFCSpreadAttributesProps> = (props) => {
const { children, ...restProps } = props;
return (
<div {...restProps}>
{children}
</div>
);
};
SHOW USAGE
import * as React from 'react';
import { SFCSpreadAttributes } from '@src/components';
export default () => (
<SFCSpreadAttributes
style={{ backgroundColor: 'lightcyan' }}
/>
);
import * as React from 'react';
export interface StatefulCounterProps {
label: string,
}
type State = {
count: number,
};
export class StatefulCounter extends React.Component<StatefulCounterProps, State> {
state: State = {
count: 0,
};
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
{label}: {count}
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
}
}
SHOW USAGE
import * as React from 'react';
import { StatefulCounter } from '@src/components';
export default () => (
<StatefulCounter
label={'StatefulCounter'}
/>
);
import * as React from 'react';
export interface StatefulCounterWithInitialCountProps {
label: string,
initialCount?: number,
}
interface DefaultProps {
initialCount: number,
}
type PropsWithDefaults = StatefulCounterWithInitialCountProps & DefaultProps;
interface State {
count: number,
}
export const StatefulCounterWithInitialCount: React.ComponentClass<StatefulCounterWithInitialCountProps> =
class extends React.Component<PropsWithDefaults, State> {
// to make defaultProps strictly typed we need to explicitly declare their type
// @see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11640
static defaultProps: DefaultProps = {
initialCount: 0,
};
state: State = {
count: this.props.initialCount,
};
componentWillReceiveProps({ initialCount }: PropsWithDefaults) {
if (initialCount != null && initialCount !== this.props.initialCount) {
this.setState({ count: initialCount });
}
}
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
{label}: {count}
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
}
};
SHOW USAGE
import * as React from 'react';
import { StatefulCounterWithInitialCount } from '@src/components';
export default () => (
<StatefulCounterWithInitialCount
label={'StatefulCounter'}
initialCount={10}
/>
);
- easily create typed component variations and reuse common logic
- especially useful to create typed list components
import * as React from 'react';
export interface GenericListProps<T> {
items: T[],
itemRenderer: (item: T) => JSX.Element,
}
export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {
render() {
const { items, itemRenderer } = this.props;
return (
<div>
{items.map(itemRenderer)}
</div>
);
}
}
SHOW USAGE
import * as React from 'react';
import { IUser } from '@src/models';
import { GenericList } from '@src/components';
export const UserList = class extends GenericList<IUser> { };
export default ({ users }: { users: IUser[] }) => (
<UserList
items={users}
itemRenderer={(item) => <div key={item.id}>{item.fullName}</div>}
/>
);
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { actionCreators } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
const mapStateToProps = (state: RootState) => ({
count: state.counters.sfcCounter,
});
export const SFCCounterConnected = connect(mapStateToProps, {
onIncrement: actionCreators.incrementSfc,
})(SFCCounter);
SHOW USAGE
import * as React from 'react';
import { SFCCounterConnected } from '@src/connected';
export default () => (
<SFCCounterConnected
label={'SFCCounterConnected'}
/>
);
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { RootState, Dispatch } from '@src/redux';
import { actionCreators } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
const mapStateToProps = (state: RootState) => ({
count: state.counters.sfcCounter,
});
const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
onIncrement: actionCreators.incrementSfc,
}, dispatch);
export const SFCCounterConnectedVerbose =
connect(mapStateToProps, mapDispatchToProps)(SFCCounter);
SHOW USAGE
import * as React from 'react';
import { SFCCounterConnectedVerbose } from '@src/connected';
export default () => (
<SFCCounterConnectedVerbose
label={'SFCCounterConnectedVerbose'}
/>
);
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { actionCreators } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
export interface SFCCounterConnectedExtended {
initialCount: number,
}
const mapStateToProps = (state: RootState, ownProps: SFCCounterConnectedExtended) => ({
count: state.counters.sfcCounter + ownProps.initialCount,
});
export const SFCCounterConnectedExtended = connect(mapStateToProps, {
onIncrement: actionCreators.incrementSfc,
})(SFCCounter);
SHOW USAGE
import * as React from 'react';
import { SFCCounterConnectedExtended } from '@src/connected';
export default () => (
<SFCCounterConnectedExtended
label={'SFCCounterConnectedExtended'}
initialCount={10}
/>
);
- function that takes a component and returns a new component
- a new component will infer Props interface from wrapped Component extended with Props of HOC
- will filter out props specific to HOC, and the rest will be passed through to wrapped component
enhance stateless counter with state
import * as React from 'react';
import { Omit } from '@src/types/react-redux-typescript';
interface RequiredProps {
count: number,
onIncrement: () => any,
}
type Props<T extends RequiredProps> = Omit<T, keyof RequiredProps>;
interface State {
count: number,
}
export function withState<WrappedComponentProps extends RequiredProps>(
WrappedComponent: React.ComponentType<WrappedComponentProps>,
) {
const HOC = class extends React.Component<Props<WrappedComponentProps>, State> {
state: State = {
count: 0,
};
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { handleIncrement } = this;
const { count } = this.state;
return (
<WrappedComponent
count={count}
onIncrement={handleIncrement}
/>
);
}
};
return HOC;
}
SHOW USAGE
import * as React from 'react';
import { withState } from '@src/hoc';
import { SFCCounter } from '@src/components';
const SFCCounterWithState =
withState(SFCCounter);
export default (
({ children }) => (
<SFCCounterWithState label={'SFCCounterWithState'} />
)
) as React.SFC<{}>;
add error handling with componentDidCatch to view component
import * as React from 'react';
const MISSING_ERROR = 'Error was swallowed during propagation.';
interface Props {
}
interface State {
error: Error | null | undefined,
}
interface WrappedComponentProps {
onReset: () => any,
}
export function withErrorBoundary(
WrappedComponent: React.ComponentType<WrappedComponentProps>,
) {
const HOC = class extends React.Component<Props, State> {
state: State = {
error: undefined,
};
componentDidCatch(error: Error | null, info: object) {
this.setState({ error: error || new Error(MISSING_ERROR) });
this.logErrorToCloud(error, info);
}
logErrorToCloud = (error: Error | null, info: object) => {
// TODO: send error report to cloud
}
handleReset = () => {
this.setState({ error: undefined });
}
render() {
const { children } = this.props;
const { error } = this.state;
if (error) {
return (
<WrappedComponent onReset={this.handleReset} />
);
}
return children as any;
}
};
return HOC;
}
SHOW USAGE
import * as React from 'react';
import { withErrorBoundary } from '@src/hoc';
import { ErrorMessage } from '@src/components';
const ErrorMessageWithErrorBoundary =
withErrorBoundary(ErrorMessage);
export default (
({ children }) => (
<ErrorMessageWithErrorBoundary>
{children}
</ErrorMessageWithErrorBoundary>
)
) as React.SFC<{}>;
This pattern is focused on a KISS principle - to stay clear of complex proprietary abstractions and follow simple and familiar JavaScript const based types:
- classic const based types
- very close to standard JS usage
- standard amount of boilerplate
- need to export action types and action creators to re-use in other places, e.g.
redux-saga
orredux-observable
export const INCREMENT_SFC = 'INCREMENT_SFC';
export const DECREMENT_SFC = 'DECREMENT_SFC';
export type Actions = {
INCREMENT_SFC: {
type: typeof INCREMENT_SFC,
},
DECREMENT_SFC: {
type: typeof DECREMENT_SFC,
},
};
// Action Creators
export const actionCreators = {
incrementSfc: (): Actions[typeof INCREMENT_SFC] => ({
type: INCREMENT_SFC,
}),
decrementSfc: (): Actions[typeof DECREMENT_SFC] => ({
type: DECREMENT_SFC,
}),
};
SHOW USAGE
import store from '@src/store';
import { actionCreators } from '@src/redux/counters';
// store.dispatch(actionCreators.incrementSfc(1)); // Error: Expected 0 arguments, but got 1.
store.dispatch(actionCreators.incrementSfc()); // OK => { type: "INCREMENT_SFC" }
A more DRY approach, introducing a simple factory function to automate the creation of typed action creators. The advantage here is that we can reduce boilerplate and code repetition. It is also easier to re-use action creators in other places because of type
property on action creator containing type constant:
- using factory function to automate creation of typed action creators - (source code to be revealed)
- less boilerplate and code repetition than KISS Style
- action creators have readonly
type
property (this make usingtype constants
redundant and easier to re-use in other places e.g.redux-saga
orredux-observable
)
import { createActionCreator } from 'react-redux-typescript';
type Severity = 'info' | 'success' | 'warning' | 'error';
// Action Creators
export const actionCreators = {
incrementCounter: createActionCreator('INCREMENT_COUNTER'),
showNotification: createActionCreator(
'SHOW_NOTIFICATION', (message: string, severity?: Severity) => ({ message, severity }),
),
};
// Examples
store.dispatch(actionCreators.incrementCounter(4)); // Error: Expected 0 arguments, but got 1.
store.dispatch(actionCreators.incrementCounter()); // OK: { type: "INCREMENT_COUNTER" }
actionCreators.incrementCounter.type === "INCREMENT_COUNTER" // true
store.dispatch(actionCreators.showNotification()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.showNotification('Hello!')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!' } }
store.dispatch(actionCreators.showNotification('Hello!', 'info')); // OK: { type: "SHOW_NOTIFICATION", payload: { message: 'Hello!', severity: 'info } }
actionCreators.showNotification.type === "SHOW_NOTIFICATION" // true
Relevant TypeScript Docs references:
- Discriminated Union types
- Mapped types e.g.
Readonly
&Partial
Declare reducer State
type definition with readonly modifier for type level
immutability
export type State = {
readonly counter: number,
};
Readonly modifier allow initialization, but will not allow rassignment highlighting an error
export const initialState: State = {
counter: 0,
}; // OK
initialState.counter = 3; // Error, cannot be mutated
This means that readonly modifier does not propagate immutability on nested properties of objects or arrays of objects. You'll need to set it explicitly on each nested property.
export type State = {
readonly counterContainer: {
readonly readonlyCounter: number,
mutableCounter: number,
}
};
state.counterContainer = { mutableCounter: 1 }; // Error, cannot be mutated
state.counterContainer.readonlyCounter = 1; // Error, cannot be mutated
state.counterContainer.mutableCounter = 1; // No error, can be mutated
There are few utilities to help you achieve nested immutability. e.g. you can do it quite easily by using convenient
Readonly
orReadonlyArray
mapped types.
export type State = Readonly<{
countersCollection: ReadonlyArray<Readonly<{
readonlyCounter1: number,
readonlyCounter2: number,
}>>,
}>;
state.countersCollection[0] = { readonlyCounter1: 1, readonlyCounter2: 1 }; // Error, cannot be mutated
state.countersCollection[0].readonlyCounter1 = 1; // Error, cannot be mutated
state.countersCollection[0].readonlyCounter2 = 1; // Error, cannot be mutated
There are some experiments in the community to make a
ReadonlyRecursive
mapped type, but I'll need to investigate if they really works
import { combineReducers } from 'redux';
import { RootAction } from '@src/redux';
import {
INCREMENT_SFC,
DECREMENT_SFC,
} from './';
export type State = {
readonly sfcCounter: number,
};
export const reducer = combineReducers<State, RootAction>({
sfcCounter: (state = 0, action) => {
switch (action.type) {
case INCREMENT_SFC:
return state + 1;
case DECREMENT_SFC:
return state + 1;
default:
return state;
}
},
});
export const reducer: Reducer<State> =
(state = 0, action: RootAction) => {
switch (action.type) {
case actionCreators.increment.type:
return state + 1;
case actionCreators.decrement.type:
return state - 1;
default: return state;
}
};
- should be imported in layers dealing with redux actions like: reducers, redux-sagas, redux-observables
// RootActions
import { RouterAction, LocationChangeAction } from 'react-router-redux';
import { Actions as CountersActions } from '@src/redux/counters';
import { Actions as TodosActions } from '@src/redux/todos';
import { Actions as ToastsActions } from '@src/redux/toasts';
type ReactRouterAction = RouterAction | LocationChangeAction;
export type RootAction =
| ReactRouterAction
| CountersActions[keyof CountersActions]
| TodosActions[keyof TodosActions]
| ToastsActions[keyof ToastsActions];
- should be imported in connected components providing type safety to Redux
connect
function
import { combineReducers } from 'redux';
import { routerReducer as router, RouterState } from 'react-router-redux';
import { reducer as counters, State as CountersState } from '@src/redux/counters';
import { reducer as todos, State as TodosState } from '@src/redux/todos';
interface StoreEnhancerState { }
export interface RootState extends StoreEnhancerState {
router: RouterState,
counters: CountersState,
todos: TodosState,
}
import { RootAction } from '@src/redux';
export const rootReducer = combineReducers<RootState, RootAction>({
router,
counters,
todos,
});
- creating store - use
RootState
(incombineReducers
and when providing preloaded state object) to set-up state object type guard to leverage strongly typed Store instance
import { createStore, applyMiddleware, compose } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { rootReducer, rootEpic, RootState } from '@src/redux';
const composeEnhancers = (
process.env.NODE_ENV === 'development' &&
window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) || compose;
function configureStore(initialState?: RootState) {
// configure middlewares
const middlewares = [
createEpicMiddleware(rootEpic),
];
// compose enhancers
const enhancer = composeEnhancers(
applyMiddleware(...middlewares),
);
// create store
return createStore(
rootReducer,
initialState!,
enhancer,
);
}
// pass an optional param to rehydrate state on app start
const store = configureStore();
// export store singleton instance
export default store;
// import rxjs operators somewhere...
import { combineEpics, Epic } from 'redux-observable';
import { RootAction, RootState } from '@src/redux';
import { saveState } from '@src/services/local-storage-service';
const SAVING_DELAY = 1000;
// persist state in local storage every 1s
const saveStateInLocalStorage: Epic<RootAction, RootState> = (action$, store) => action$
.debounceTime(SAVING_DELAY)
.do((action: RootAction) => {
// handle side-effects
saveState(store.getState());
})
.ignoreElements();
export const epics = combineEpics(
saveStateInLocalStorage,
);
import { createSelector } from 'reselect';
import { RootState } from '@src/redux';
export const getTodos =
(state: RootState) => state.todos.todos;
export const getTodosFilter =
(state: RootState) => state.todos.todosFilter;
export const getFilteredTodos = createSelector(
getTodos, getTodosFilter,
(todos, todosFilter) => {
switch (todosFilter) {
case 'completed':
return todos.filter((t) => t.completed);
case 'active':
return todos.filter((t) => !t.completed);
default:
return todos;
}
},
);
- Recommended setup for best benefits from type-checking, with support for JSX and ES2016 features
- Add
tslib
to minimize bundle size:npm i tslib
- this will externalize helper functions generated by transpiler and otherwise inlined in your modules- Include absolute imports config working with Webpack
{
"compilerOptions": {
"baseUrl": "./", // enables absolute path imports
"paths": { // define absolute path mappings
"@src/*": ["src/*"] // will enable -> import { ... } from '@src/components'
// in webpack you need to add -> resolve: { alias: { '@src': PATH_TO_SRC } }
},
"outDir": "dist/", // target for compiled files
"allowSyntheticDefaultImports": true, // no errors on commonjs default import
"allowJs": true, // include js files
"checkJs": true, // typecheck js files
"declaration": false, // don't emit declarations
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true, // importing helper functions from tslib
"noEmitHelpers": true, // disable emitting inline helper functions
"jsx": "react", // process JSX
"lib": [
"dom",
"es2016",
"es2017.object"
],
"target": "es5", // "es2015" for ES6+ engines
"module": "commonjs", // "es2015" for tree-shaking
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"pretty": true,
"removeComments": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"src/**/*.spec.*"
]
}
- Recommended setup is to extend build-in preset
tslint:recommended
(for all rules usetslint:all
)- Add tslint react rules:
npm i -D tslint-react
https://github.com/palantir/tslint-react- Amended some extended defaults for more flexibility
{
"extends": ["tslint:recommended", "tslint-react"],
"rules": {
"arrow-parens": false,
"arrow-return-shorthand": [false],
"comment-format": [true, "check-space"],
"import-blacklist": [true, "rxjs"],
"interface-over-type-literal": false,
"max-line-length": [true, 120],
"member-access": false,
"member-ordering": [true, {
"order": "fields-first"
}],
"newline-before-return": false,
"no-any": false,
"no-empty-interface": false,
"no-import-side-effect": [true],
"no-inferrable-types": [true, "ignore-params", "ignore-properties"],
"no-invalid-this": [true, "check-function-in-method"],
"no-null-keyword": false,
"no-require-imports": false,
"no-switch-case-fall-through": true,
"no-submodule-imports": [true, "rxjs", "@src"],
"no-trailing-whitespace": true,
"no-this-assignment": [true, {
"allow-destructuring": true
}],
"no-unused-variable": [true, "react"],
"object-literal-sort-keys": false,
"object-literal-shorthand": false,
"one-variable-per-declaration": [false],
"only-arrow-functions": [true, "allow-declarations"],
"ordered-imports": [false],
"prefer-method-signature": false,
"prefer-template": [true, "allow-single-concat"],
"semicolon": [true, "ignore-interfaces"],
"quotemark": [true, "single", "jsx-double"],
"triple-equals": [true, "allow-null-check"],
"typedef": [true,"parameter", "property-declaration"],
"variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"]
}
}
- Recommended setup for Jest with TypeScript
- Install with
npm i -D jest-cli ts-jest @types/jest
{
"verbose": true,
"transform": {
".(ts|tsx)": "./node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"globals": {
"window": {},
"ts-jest": {
"tsConfigFile": "./tsconfig.json"
}
},
"setupFiles": [
"./jest.stubs.js",
"./src/rxjs-imports.tsx"
]
}
Most flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
Using this solution you'll achieve better encapsulation for internal structure/naming refactoring without breaking your consumer code:
// 1. in `components/` folder create component file (`select.tsx`) with default export:
// components/select.tsx
const Select: React.SFC<Props> = (props) => {
...
export default Select;
// 2. in `components/` folder create `index.ts` file handling named imports:
// components/index.ts
export { default as Select } from './select';
...
// 3. now you can import your components in both ways named (internal) or default (public):
// containers/container.tsx
import { Select } from '@src/components';
or
import Select from '@src/components/select';
...
Strategies to fix issues coming from broken "vendor type declarations" files (*.d.ts)
- Augmenting library internal type declarations - using relative import resolution
// added missing autoFocus Prop on Input component in "[email protected]" npm package
declare module '../node_modules/antd/lib/input/Input' {
export interface InputProps {
autoFocus?: boolean;
}
}
- Augmenting library public type declarations - using node module import resolution
// fixed broken public type declaration in "[email protected]" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';
declare module 'rxjs/Subject' {
interface Subject<T> {
lift<R>(operator: Operator<T, R>): Observable<R>;
}
}
- When missing type declarations for vendor modules you can "assert" a module type with
any
using Shorthand Ambient Modules
// @src/types/modules.d.ts
declare module 'react-test-renderer';
declare module 'enzyme';
More advanced scenarios for working with vendor module declarations can be found here Official TypeScript Docs
Common TS-related npm scripts shared across projects
"check": "npm run lint & npm run tsc",
"lint": "tslint --project './tsconfig.json'",
"tsc": "tsc -p . --noEmit",
"tsc:watch": "tsc -p . --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
No. When using TypeScript it is an unnecessary overhead, when declaring IProps and IState interfaces, you will get complete intellisense and compile-time safety with static type checking, this way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standarized method of documenting your component external API in the source code.
From practical side, using
interface
declaration will display identity (interface name) in compiler errors, on the contrarytype
aliases will be unwinded to show all the properties and nested types it consists of. This can be a bit noisy when reading compiler errors and I like to leverage this distinction to hide some of not so important type details in errors
Relatedts-lint
rule: https://palantir.github.io/tslint/rules/interface-over-type-literal/
Prefered modern style is to use class Property Initializers
class StatefulCounterWithInitialCount extends React.Component<Props, State> {
// default props using Property Initializers
static defaultProps: DefaultProps = {
className: 'default-class',
initialCount: 0,
};
// initial state using Property Initializers
state: State = {
count: this.props.initialCount,
};
...
}
Prefered modern style is to use Class Fields with arrow functions
class StatefulCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
...
}
https://github.com/piotrwitek/react-redux-typescript-starter-kit
https://github.com/piotrwitek/react-redux-typescript-webpack-starter