이 가이드는 타입스크립트를 사용해서 React & Redux 앱을 작성하는 포괄적인 가이드입니다.
타입스크립트2.2 로드맵 (https://github.com/Microsoft/TypeScript/wiki/Roadmap)
- 완벽하고 안전하게 any타입에서 벗어나기.
- Type Inference을 이용해서 수동으로 입력하는 주석의 양 최소화
- Generics 및 Advanced Types 기능을 사용하여 심플한 유틸리티 기능으로 boilerplate를 제작.
- React
- Redux
- Ecosystem
- Async Flow with "redux-observable"
- Selectors with "reselect"
- Forms with "formstate" WIP
- Styles with "typestyle" WIP
- Extras
- FAQ
- Project Examples
- state가 없는 component 샘플 코드 (멈청한 컴포넌트 글래스)
import * as React from 'react';
type Props = {
className?: string,
style?: React.CSSProperties,
};
const MyComponent: React.StatelessComponent<Props> = (props) => {
const { children, ...restProps } = props;
return (
<div {...restProps} >
{children}
</div>
);
};
export default MyComponent;
- class component 샘플 코드 (일반적인 컴포넌트 클래스)
import * as React from 'react';
type Props = {
className?: string,
style?: React.CSSProperties,
initialCount?: number,
};
type State = {
counter: number,
};
class MyComponent extends React.Component<Props, State> {
// Property Initializers를 이용하여 Props의 기본값을 지정하는 부분
static defaultProps: Partial<Props> = {
className: 'default-class',
};
// Property Initializers를 이용하여 State의 기본값을 지정하는 부분
state: State = {
counter: this.props.initialCount || 0,
};
// 라이프 사이클 메소드는 일반적인 인스턴스 메소드로 선언되어야하며 아무 문제 없습니다.
componentDidMount() {
// tslint:disable-next-line:no-console
console.log('Mounted!');
}
// 화살표 함수가있는 클래스 필드를 사용하는 핸들러
increaseCounter = () => {
this.setState({
counter: this.state.counter + 1
});
};
render() {
const { children, initialCount, ...restProps } = this.props;
return (
<div {...restProps} onClick={this.increaseCounter} >
Clicks: {this.state.counter}
<hr />
{children}
</div>
);
}
}
export default MyComponent;
- 새로운 Component를 반환하는 Component랩핑 또는 decorate를 생성합니다.
- 새로운 Component는 'HOC'의 Props와 함께 확장된 입력 composition을 통해 props인터페이스를 상속받습니다.
- Type Inference을 이용해서 결과적인 Props 인터페이스를 자동으로 계산합니다.
- decorate Props를 필터링 하고 관련된 Props 만 랩핑된 Component에 전달합니다.
- stateless functional 또는 규칙적인 Component 포함해서 제작합니다.
// controls/button.tsx
import * as React from 'react';
import { Button } from 'antd';
type Props = {
className?: string,
autoFocus?: boolean,
htmlType?: typeof Button.prototype.props.htmlType,
type?: typeof Button.prototype.props.type,
};
const ButtonControl: React.StatelessComponent<Props> = (props) => {
const { children, ...restProps } = props;
return (
<Button {...restProps} >
{children}
</Button>
);
};
export default ButtonControl;
// decorators/with-form-item.tsx
import * as React from 'react';
import { Form } from 'antd';
const FormItem = Form.Item;
type BaseProps = {
};
type HOCProps = FormItemProps & {
error?: string;
};
type FormItemProps = {
label?: typeof FormItem.prototype.props.label;
labelCol?: typeof FormItem.prototype.props.labelCol;
wrapperCol?: typeof FormItem.prototype.props.wrapperCol;
required?: typeof FormItem.prototype.props.required;
help?: typeof FormItem.prototype.props.help;
validateStatus?: typeof FormItem.prototype.props.validateStatus;
colon?: typeof FormItem.prototype.props.colon;
};
export function withFormItem<WrappedComponentProps extends BaseProps>(
WrappedComponent:
React.StatelessComponent<WrappedComponentProps> | React.ComponentClass<WrappedComponentProps>,
) {
const HOC: React.StatelessComponent<HOCProps & WrappedComponentProps> =
(props) => {
const {
label, labelCol, wrapperCol, required, help, validateStatus, colon,
error, ...passThroughProps,
} = props as HOCProps;
// 빈 decorate props를 필터링
const formItemProps: FormItemProps = Object.entries({
label, labelCol, wrapperCol, required, help, validateStatus, colon,
}).reduce((definedProps: any, [key, value]) => {
if (value !== undefined) { definedProps[key] = value; }
return definedProps;
}, {});
// 조건에 따라 추가적 props을 주입(injecting)
if (error) {
formItemProps.help = error;
formItemProps.validateStatus = 'error';
}
return (
<FormItem {...formItemProps} hasFeedback={true} >
<WrappedComponent {...passThroughProps as any} />
</FormItem>
);
};
return HOC;
}
// components/consumer-component.tsx
...
import { Button, Input } from '../controls';
import { withFormItem, withFieldState } from '../decorators';
// decorator를 사용해서 좀 더 전문화된 component를 만들수 있습니다.
const ButtonField = withFormItem(Button);
// function composition를 활용하면 여러개의 decorator를 생성할수 있습니다.
const InputFieldWithState = withFormItem(withFieldState(Input));
// 향상된 component는 HOC가 적용된 컴포넌트에서 props type을 상속받습니다.
<ButtonField type="primary" htmlType="submit" wrapperCol={{ offset: 4, span: 12 }} autoFocus={true} >
Next Step
</ButtonField>
...
<InputFieldWithState {...formFieldLayout} label="Type" required={true} autoFocus={true}
fieldState={configurationTypeFieldState} error={configurationTypeFieldState.error}
/>
...
// 필요에 따라 ramda 또는 lodash와 같은 기능적 라이브러리를 사용할 수 있습니다.
const InputFieldWithState = compose(withFormItem, withFieldStateInput)(Input);
// 참고 : compose 함수는 type declarations이 필요하거나 type inference를 잃을 수 있습니다
참고: connect함수의 type inference는 완전한 형태의 안전성을 제공하지 않고 자동으로 [
Higher-Order Component
] (#higher-order-component) 위의 예에서와 같이 소품 인터페이스를 결과 계산하는 타입 추론을 활용하지 않을 것입니다..
위의 문제는 내가 해결해보려고 한 문제입니다.
- 이 해결책은 type inference을 사용하여
mapStateToProps
함수로부터 Props 타입을 얻습니다. connect
도우미 함수로부터 주입 된 Props 유형을 선언하고 유지하기위한 수작업을 최소화합니다.- TypeScript가 아직 이 기능을 지원하지 않기 때문에
returntypeof()
헬퍼 함수를 사용합니다. (https://github.com/piotrwitek/react-redux-typescript#returntypeof-polyfill) - Real project example: https://github.com/piotrwitek/react-redux-typescript-starter-kit/blob/ef2cf6b5a2e71c55e18ed1e250b8f7cadea8f965/src/containers/currency-converter-container/index.tsx
import { returntypeof } from 'react-redux-typescript';
import { RootState } from '../../store/types';
import { increaseCounter, changeBaseCurrency } from '../../store/action-creators';
import { getCurrencies } from '../../store/state/currency-rates/selectors';
const mapStateToProps = (rootState: RootState) => ({
counter: rootState.counter,
baseCurrency: rootState.baseCurrency,
currencies: getCurrencies(rootState),
});
const dispatchToProps = {
increaseCounter: increaseCounter,
changeBaseCurrency: changeBaseCurrency,
};
// mapStateToProps 및 dispatchToProps에서 유추 된 Props types
const stateProps = returntypeof(mapStateToProps);
type Props = typeof stateProps & typeof dispatchToProps;
class CurrencyConverterContainer extends React.Component<Props, {}> {
handleInputBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
const intValue = parseInt(ev.currentTarget.value, 10);
this.props.increaseCounter(intValue); // number
}
handleSelectChange = (ev: React.ChangeEvent<HTMLSelectElement>) => {
this.props.changeBaseCurrency(ev.target.value); // string
}
render() {
const { counter, baseCurrency, currencies } = this.props; // number, string, string[]
return (
<section>
<input type="text" value={counter} onBlur={handleInputBlur} ... />
<select value={baseCurrency} onChange={handleSelectChange} ... >
{currencies.map(currency =>
<option key={currency}>{currency}</option>
)}
</select>
...
</section>
);
}
}
export default connect(mapStateToProps, dispatchToProps)(CurrencyConverterContainer);
이 전근법에서 저는 KISS 원칙을 준수했고, 많은 TypeScript Redux 가이드에서 흔히 볼 수있는 독점적 인 추상화를 벗어나, 익숙한 자바스크립트 사용에 최대한 가깝지만 static types의 이점을 얻으려 했습니다. 그 방법으로는,
- 전통적인 const 타입을 사용
- 표준에 가까운 JS 사용
- 표준 boilerplate
redux-saga
또는redux-observable
과 같은 다른 모듈에서 재사용하기 위해 Action Types과 Action Creators를 내 보내야합니다.
DEMO: TypeScript Playground
// Action Types
export const INCREASE_COUNTER = 'INCREASE_COUNTER';
export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';
// Action Creators
export const actionCreators = {
increaseCounter: () => ({
type: INCREASE_COUNTER as typeof INCREASE_COUNTER,
}),
changeBaseCurrency: (payload: string) => ({
type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
}),
}
// Examples
store.dispatch(actionCreators.increaseCounter(4)); // Error: 제공하는 매개 변수가 대상의 인수와 일치하지 않음.
store.dispatch(actionCreators.increaseCounter()); // OK => { type: "INCREASE_COUNTER" }
store.dispatch(actionCreators.changeBaseCurrency()); // Error: 제공하는 매개 변수가 대상의 인수와 일치하지 않음.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }
다른 대안으로 DRY 원칙, 입력 된 액션 제작자의 생성을 자동화하는 간단한 도우미 팩토리 함수를 소개합니다. 여기서 장점은 일부 상용구 및 코드 반복을 줄일 수 있다는 것입니다. 액션 생성자를 다른 모듈에서 재사용하는 것이 더 쉽습니다.
- 팩토리 함수를 이용해서 입력 된 액션 제작자를 자동으로 생성
- KISS원칙 보다 적은 상용구와 코드 반복
- redux-saga또는 같은 다른 모듈에서 재사용하기 쉽다 redux-observable(액션 생성자는 타입 프로퍼티를 가지며, 타입 상수는 중복된다)
DEMO: WIP
import { createActionCreator } from 'react-redux-typescript';
// Action Creators
export const actionCreators = {
increaseCounter: createActionCreator('INCREASE_COUNTER'), // { type: "INCREASE_COUNTER" }
changeBaseCurrency: createActionCreator('CHANGE_BASE_CURRENCY', (payload: string) => payload), // { type: "CHANGE_BASE_CURRENCY", payload: string }
showNotification: createActionCreator('SHOW_NOTIFICATION', (payload: string, meta?: { type: string }) => payload),
};
// Examples
store.dispatch(actionCreators.increaseCounter(4)); // Error: 제공하는 매개 변수가 대상의 인수와 일치하지 않음.
store.dispatch(actionCreators.increaseCounter()); // OK: { type: "INCREASE_COUNTER" }
actionCreators.increaseCounter.type === "INCREASE_COUNTER" // true
store.dispatch(actionCreators.changeBaseCurrency()); // Error: 제공하는 매개 변수가 대상의 인수와 일치하지 않음.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK: { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }
actionCreators.changeBaseCurrency.type === "CHANGE_BASE_CURRENCY" // true
store.dispatch(actionCreators.showNotification()); // Error: 제공하는 매개 변수가 대상의 인수와 일치하지 않음.
store.dispatch(actionCreators.showNotification('Hello!')); // OK: { type: "SHOW_NOTIFICATION", payload: 'Hello!' }
store.dispatch(actionCreators.showNotification('Hello!', { type: 'warning' })); // OK: { type: "SHOW_NOTIFICATION", payload: 'Hello!', meta: { type: 'warning' } }
actionCreators.showNotification.type === "SHOW_NOTIFICATION" // true
관련 TypeScript 문서 참조:
- Discriminated Union types
- Mapped types like
Readonly
&Partial
DEMO: TypeScript Playground
// 1a. readonly를 사용한 타입으로 state, props을 불편으로 표시하고 컴파일러를 이용해 모든 변형에 대해 보호합니다.
export type State = {
readonly counter: number,
readonly baseCurrency: string,
};
// 1b. 원하는 경우 'Readonly'맵핑된 타입을 사용할 수 있습니다.
export type StateAlternative = Readonly<{
counter: number,
baseCurrency: string,
}>;
// 2. 초기 state 선언 -> Note: 수정 자만 초기화 할 수 있습니다. (일기 전용 속성)
export const initialState: State = {
counter: 0,
baseCurrency: 'EUR',
};
initialState.counter = 3; // Error: 읽기 전용 속성이므로 'counter'에 할당할 수 없습니다.
- 전통적인 const 타입을 사용
- 단일 prop 업데이트 또는 간단한 state objects에 적합
DEMO: TypeScript Playground
import { Action } from '../../types';
export default function reducer(state: State = initialState, action: Action): State {
switch (action.type) {
case INCREASE_COUNTER:
return {
...state, counter: state.counter + 1, // no payload
};
case CHANGE_BASE_CURRENCY:
return {
...state, baseCurrency: action.payload, // payload: string
};
default: return state;
}
}
헬퍼 팩토리 함수에서
actionCreator
에 정적 statictype
속성 사용하기
- if의 "block scope"는 복잡한 상태 업데이트 로직에서 로컬변수를 사용할 수 있게 해줍나다.
- actionCreator의 static type속성을 on으로 사용하면 액션 유형 상수를 제거 할 수 있으므로 액션 을 쉽게 만들 수 있고 reducer, sagas, epics 등의 액션 유형을 확인하기 위해 다시 사용할 수 있습니다.
DEMO: WIP
import { Action } from '../../types';
// Reducer
export default function reducer(state: State = initialState, action: Action): State {
switch (action.type) {
case actionCreators.increaseCounter.type:
return {
...state, counter: state.counter + 1, // no payload
};
case actionCreators.changeBaseCurrency.type:
return {
...state, baseCurrency: action.payload, // payload: string
};
default: return state;
}
}
partialState
객체를 사용하면 spread operation 중에 보호된 객체에 병합할 수 있습니다. - 이렇게 하면 객체가 초기회 되거나 일치하지 않는 속성을 가지는 것을 방지하고 state interface와 정홧히 일치 합니다.
WARNING: 이 해결법은 spread operation 중에 TypeScript 컴파일러가 불필요하거나 불일치하는 props으로부터 보호 해주지 않기 때문에 현재로서는 필수입니다.
PS: There is an Exact Type proposal to improve this behaviour
DEMO: TypeScript Playground
import { Action } from '../../types';
// BAD
export function badReducer(state: State = initialState, action: Action): State {
if (action.type === INCREASE_COUNTER) {
return {
...state,
counterTypoError: state.counter + 1, // counterTypoError가 정의 되지 않아도 오류를 내보내지 않습니다.
}; // 컴파일 오류가 예상되고, state와 정확히 일치하는것을 보장하지 못합니다/
}
}
// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
let partialState: Partial<State> | undefined;
if (action.type === INCREASE_COUNTER) {
partialState = {
counterTypoError: state.counter + 1, // counterTypoError가 정의되어 있지 않다면 state에 정의된 속성만 리터럴 할수 잇게 방어 할수 있습니다.
}; // 이제 에러를 올바르게 표현합니다.
}
if (action.type === CHANGE_BASE_CURRENCY) {
partialState = { // 속성의 오류를 잘 잡아 냅니다. 'string'속성에 'number'를 지정할수 없습니다.
baseCurrency: 5,
}; // 타입 오류를 잘 잡아냅니다.
}
return partialState != null ? { ...state, ...partialState } : state;
}
- redux, redux-sagas, redux-observables와 같은 redux 액션을 다루는 레이어로 가져와야합니다.
import { returntypeof } from 'react-redux-typescript';
import * as actionCreators from './action-creators';
const actions = Object.values(actionCreators).map(returntypeof);
export type Action = typeof actions[number];
- Redux connect기능에 유형안전성을 제공하는 연결된 구성 요소를 가져와야 합니다.
import {
reducer as currencyRatesReducer, State as CurrencyRatesState,
} from './state/currency-rates/reducer';
import {
reducer as currencyConverterReducer, State as CurrencyConverterState,
} from './state/currency-converter/reducer';
export type RootState = {
currencyRates: CurrencyRatesState;
currencyConverter: CurrencyConverterState;
};
- 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 { combineReducers, createStore } from 'redux';
import { RootState } from '../types';
const rootReducer = combineReducers<RootState>({
currencyRates: currencyRatesReducer,
currencyConverter: currencyConverterReducer,
});
// rehydrating state on app start: implement here...
const recoverState = (): RootState => ({} as RootState);
export const store = createStore(
rootReducer,
recoverState(),
);
- composing enhancers - example of setting up
redux-observable
middleware
declare var window: Window & { devToolsExtension: any, __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any };
import { createStore, compose, applyMiddleware } from 'redux';
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import { epics as currencyConverterEpics } from './currency-converter/epics';
const rootEpic = combineEpics(
currencyConverterEpics,
);
const epicMiddleware = createEpicMiddleware(rootEpic);
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// store singleton instance
export const store = createStore(
rootReducer,
recoverState(),
composeEnhancers(applyMiddleware(epicMiddleware)),
);
import 'rxjs/add/operator/map';
import { combineEpics, Epic } from 'redux-observable';
import { RootState, Action } from '../types'; // check store section
import { actionCreators } from '../reducer';
import { convertValueWithBaseRateToTargetRate } from './utils';
import * as currencyConverterSelectors from './selectors';
import * as currencyRatesSelectors from '../currency-rates/selectors';
const recalculateTargetValueOnCurrencyChange: Epic<Action, RootState> = (action$, store) =>
action$.ofType(
actionCreators.changeBaseCurrency.type,
actionCreators.changeTargetCurrency.type,
).map((action: any) => {
const value = convertValueWithBaseRateToTargetRate(
currencyConverterSelectors.getBaseValue(store.getState()),
currencyRatesSelectors.getBaseCurrencyRate(store.getState()),
currencyRatesSelectors.getTargetCurrencyRate(store.getState()),
);
return actionCreators.recalculateTargetValue(value);
});
const recalculateTargetValueOnBaseValueChange: Epic<Action, RootState> = (action$, store) =>
action$.ofType(
actionCreators.changeBaseValue.type,
).map((action: any) => {
const value = convertValueWithBaseRateToTargetRate(
action.payload,
currencyRatesSelectors.getBaseCurrencyRate(store.getState()),
currencyRatesSelectors.getTargetCurrencyRate(store.getState()),
);
return actionCreators.recalculateTargetValue(value);
});
const recalculateBaseValueOnTargetValueChange: Epic<Action, RootState> = (action$, store) =>
action$.ofType(
actionCreators.changeTargetValue.type,
).map((action: any) => {
const value = convertValueWithBaseRateToTargetRate(
action.payload,
currencyRatesSelectors.getTargetCurrencyRate(store.getState()),
currencyRatesSelectors.getBaseCurrencyRate(store.getState()),
);
return actionCreators.recalculateBaseValue(value);
});
export const epics = combineEpics(
recalculateTargetValueOnCurrencyChange,
recalculateTargetValueOnBaseValueChange,
recalculateBaseValueOnTargetValueChange,
);
import { createSelector } from 'reselect';
import { RootState } from '../types';
const getCurrencyConverter = (state: RootState) => state.currencyConverter;
const getCurrencyRates = (state: RootState) => state.currencyRates;
export const getCurrencies = createSelector(
getCurrencyRates,
(currencyRates) => {
return Object.keys(currencyRates.rates).concat(currencyRates.base);
},
);
export const getBaseCurrencyRate = createSelector(
getCurrencyConverter, getCurrencyRates,
(currencyConverter, currencyRates) => {
const selectedBase = currencyConverter.baseCurrency;
return selectedBase === currencyRates.base
? 1 : currencyRates.rates[selectedBase];
},
);
export const getTargetCurrencyRate = createSelector(
getCurrencyConverter, getCurrencyRates,
(currencyConverter, currencyRates) => {
return currencyRates.rates[currencyConverter.targetCurrency];
},
);
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
{
"compilerOptions": {
"baseUrl": "src/", // enables relative imports to root
"outDir": "out/", // 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",
"module": "es2015",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strictNullChecks": true,
"pretty": true,
"removeComments": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
Recommended setup is to extend build-in preset
tslint:latest
(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:latest", "tslint-react"],
"rules": {
"arrow-parens": false,
"arrow-return-shorthand": [false],
"comment-format": [true, "check-space"],
"import-blacklist": [true, "rxjs"],
"interface-over-type-literal": false,
"member-access": false,
"member-ordering": [true, {"order": "statics-first"}],
"newline-before-return": false,
"no-any": false,
"no-inferrable-types": [true],
"no-import-side-effect": [true, {"ignore-module": "^rxjs/"}],
"no-invalid-this": [true, "check-function-in-method"],
"no-null-keyword": false,
"no-require-imports": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-variable": [true, "react"],
"object-literal-sort-keys": false,
"only-arrow-functions": [true, "allow-declarations"],
"ordered-imports": [false],
"prefer-method-signature": false,
"prefer-template": [true, "allow-single-concat"],
"quotemark": [true, "single", "jsx-double"],
"triple-equals": [true, "allow-null-check"],
"typedef": [true,"parameter", "property-declaration", "member-variable-declaration"],
"variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case"]
}
}
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.StatelessComponent<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 like this:
// containers/container.tsx
import { Select } from '../components';
or
import Select from '../components/select';
...
Augmenting missing autoFocus Prop on
Input
andButton
components inantd
npm package (https://ant.design/).
declare module '../node_modules/antd/lib/input/Input' {
export interface InputProps {
autoFocus?: boolean;
}
}
declare module '../node_modules/antd/lib/button/Button' {
export interface ButtonProps {
autoFocus?: boolean;
}
}
// react
npm i -D @types/react @types/react-dom @types/react-redux
// redux has types included in it's npm package - don't install from @types
No. In TypeScript it is unnecessary, when declaring Props and State types (refer to React components examples) you will get completely free intellisense and compile-time safety with static type checking, this way you'll be safe from runtime errors, not waste time on debugging and get an elegant way of describing component external API in your source code.
Prefered modern style is to use class Property Initializers
class MyComponent extends React.Component<Props, State> {
// default props using Property Initializers
static defaultProps: Props = {
className: 'default-class',
initialCount: 0,
};
// initial state using Property Initializers
state: State = {
counter: this.props.initialCount,
};
...
}
Prefered modern style is to use Class Fields with arrow functions
class MyComponent extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
increaseCounter = () => { this.setState({ counter: this.state.counter + 1}); };
...
}
From practical point of view
interface
types will use it's identity when showing compiler errors, whiletype
aliases will be always unwinded to show all the nested types it consists of. This can be too noisy when reading compiler errors and I like to leverage this distinction to hide some not important type details in reported type errors Relatedts-lint
rule: https://palantir.github.io/tslint/rules/interface-over-type-literal/
https://github.com/piotrwitek/react-redux-typescript-starter-kit https://github.com/piotrwitek/react-redux-typescript-webpack-starter