TypeScript + Reduxはもうぼちぼちサードライブラリに頼らなくてもある程度はいい感じに補完してくれる ref: https://qiita.com/terrierscript/items/b3f9dd95a4c7afe0b102
export type AppAction<T extends string, Extra extends {} = {}> =
Action<T> &
{ [K in keyof Extra]: Extra[K] }
enum ActionType {
increment = "INCREMENT",
decrement = "DECREMENT",
force = "FORCE"
}
type CounterAction =
| AppAction<ActionType.increment>
| AppAction<ActionType.decrement>
| AppAction<ActionType.force, { count: number }>
const increment = (): CounterAction => {
return { type: ActionType.increment }
}
const decrement = (): CounterAction => {
return { type: ActionType.decrement }
}
const force = (num: number): CounterAction => {
return {
type: ActionType.force,
count: num
}
}
export const counterActions = { increment, decrement, force }
export const counterReducer: Reducer<number, CounterAction> = (
state = 0,
action
) => {
switch (action.type) {
case ActionType.increment:
return state + 1
case ActionType.decrement:
return state - 1
case ActionType.force:
return action.count
}
return state
}
enum ActionType {
increment = "INCREMENT",
decrement = "DECREMENT",
force = "FORCE"
}
import { Action } from "redux"
// Actionに対して、Extraな値をMappedTypeにして追加する形で認識させたものを定義。AnyActionの改良版。
export type AppAction<T extends string, Extra extends {} = {}> =
Action<T> &
{ [K in keyof Extra]: Extra[K] }
type CounterAction =
| AppAction<"INCREMENT">
| AppAction<"DECREMENT">
| AppAction<"FORCE", { count: number }>
// Flux-standardなaction。typescript-fsaからほぼパクってきた
interface AppAction<ActionName extends string, Payload = null> {
type: ActionName
payload?: Payload
error?: boolean
meta?: Object
}
type CounterAction =
| AppAction<"INCREMENT">
| AppAction<"DECREMENT">
| AppAction<"FORCE", { count: number }>
// 元
// type CounterActionType = "INCREMENT" | "DECREMENT"
// type CounterAction = Action<CounterActionType>
type CounterAction = {
type: "INCREMENT"
} | {
type: "DECREMENT"
} | {
type: "FORCE"
count: number
}
// typeof counterActionsをしてる。ただしこのactionが引数を正しくは認識出来ない
type ChildProps = StateProps & typeof counterActions
class CounterInner extends Component<ChildProps> {
render() {
// 省略
}
}
const increment = (): CounterAction => {
return { type: "INCREMENT" }
}
const decrement = (): CounterAction => {
return { type: "DECREMENT" }
}
const force = (num: number): CounterAction => { // 引数をとる例
return {
type: "FORCE",
count: num
}
}
// ここでまとめ直しが必要
export const counterActions = { increment, decrement, force }
import { Dispatch } from "redux"
import { AppState } from "~/client/store/store"
import { counterActions } from "~/client/store/counter"
type StateProps = {
cnt: number
}
type DispatchProps = {
dispatch: Dispatch
}
type ChildProps = StateProps & DispatchProps
class CounterInner extends Component<ChildProps> {
render() {
const { dispatch } = this.props
const { increment, decrement } = counterActions
return (
<div>
<div>{this.props.cnt}</div>
<button onClick={(e) => dispatch(increment())}>+</button>
<button onClick={(e) => dispatch(decrement())}>-</button>
</div>
)
}
}
export type AppActionCreators<
T extends { [key: string]: (...args) => any }
> = ReturnType<T[keyof T]>
export const counterActions = {
increment: createAppAction(ActionType.increment),
decrement: createAppAction(ActionType.decrement),
force: createAppAction(ActionType.force, (num: number) => {
return {
count: num
}
})
}
type CounterAction = AppActionCreators<typeof counterActions>
// もとの定義
// type CounterAction =
// | AppAction<ActionType.increment>
// | AppAction<ActionType.decrement>
// | AppAction<ActionType.force, { count: number }>
type CounterAction = ReturnType<
typeof increment | typeof decrement | typeof force
>
const increment = createAppAction(ActionType.increment)
const decrement = createAppAction(ActionType.decrement)
const force = createAppAction(ActionType.force, (num: number) => {
return {
count: num
}
})
// Extraの部分を作成する関数。Tuple in Rest parameter大活躍!
type ExtraFunction<Arg extends any[], R> = (...args: Arg) => R
type ActionCreator<Arg extends any[], Action> = (...args: Arg) => Action
// 関数のoverload
export function createAppAction<A extends string>(
type: A
): ActionCreator<any[], AppAction<A>>
export function createAppAction<A extends string, Arg extends any[], R>(
type: A,
fn: ExtraFunction<Arg, R>
): ActionCreator<Arg, AppAction<A, R>>
export function createAppAction(type, extraFunction?) {
return (...args) => {
if (extraFunction) {
const extra = extraFunction(...args)
return { type, ...extra }
}
return { type }
}
}
type StateProps = {
cnt: number
}
type ChildProps = StateProps & typeof counterActions
class CounterInner extends Component<ChildProps> {
render() {
const { increment, decrement } = this.props
return (
<div>
<div>{this.props.cnt}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
}
const connectCounter = connect(
(state: AppState): StateProps => ({
cnt: state.counter
}),
(dispatch) => bindActionCreators(counterActions, dispatch)
)
export const Counter = connectCounter(CounterInner)
// Counter sample
import React, { Component } from "react"
import { connect } from "react-redux"
import { bindActionCreators, Dispatch } from "redux"
import {
counterActions,
AppState,
CounterActionCreators
} from "../store"
// State -> Propsに変換する例。
// 変換不要なら type StateProps = AppState でいいだろう
type StateProps = {
cnt: number
}
// 子のPropsはStatePropsとCounterActionCreatorsを持つ
type ChildProps = StateProps & CounterActionCreators
class CounterInner extends Component<ChildProps> {
render() {
return (
<div>
<div>{this.props.cnt}</div>
<button onClick={this.props.increment}>+</button>
<button onClick={this.props.decrement}>-</button>
</div>
)
}
}
const connectCounter = connect(
(state: AppState): StateProps => ({
cnt: state.counter
}),
(dispatch: Dispatch) => bindActionCreators(counterActions, dispatch)
)
export const Counter = connectCounter(CounterInner)
import {
createStore,
Reducer,
Action,
combineReducers,
ActionCreatorsMapObject,
ReducersMapObject,
ActionCreator
} from "redux"
//// State全体の定義
export type AppState = {
counter: number
}
//// ActionとActionCreatorの定義
// type CounterAction = Action<"INCREMENT" | "DECREMENT"> でも可
type CounterActionType = "INCREMENT" | "DECREMENT"
type CounterAction = Action<CounterActionType>
// これだけだるいが利用側のために二重定義してる。後述
export interface CounterActionCreators
extends ActionCreatorsMapObject<CounterAction> {
increment: ActionCreator<CounterAction>
decrement: ActionCreator<CounterAction>
}
export const counterActions: CounterActionCreators = {
increment: () => {
return { type: "INCREMENT" }
},
decrement: () => {
return { type: "DECREMENT" }
}
}
//// Reducer。genericsらへんはちょっと怠けてる
export const counterReducer: Reducer<any, CounterAction> = (
state = 0,
action
) => {
switch (action.type) {
case "INCREMENT": // このへんVSCode補完効いて最高の気分
return state + 1
case "DECREMENT":
return state - 1
}
return state
}
export const generateStore = () => {
const reducerMap: ReducersMapObject<AppState> = {
counter: counterReducer
}
return createStore(combineReducers(reducerMap))
}