Interfaceのメンバ競合の問題

import * as ReduxActions from 'redux-actions';
import * as Redux from 'redux';

// 型の競合エラーがでる
interface ExAction extends Redux.Action, ReduxActions.Action<string> {}

現実感の無い実験のためのこのコードは、今日時点では残念ながらコンパイラを通りません。理由はInterfaceのメンバが競合しているからです。

// reduxのindex.d.tsより
export interface Action {
    type: any; // ここでtypeプロパティ宣言
}

// @types/redux-actionsのindex.d.tsより
export interface BaseAction {
    type: string; // ここでもtypeプロパティ宣言
}
export interface Action<Payload> extends BaseAction {
    payload?: Payload;
    error?: boolean;
}

どちらもtypeプロパティを持ってるけど、型がreduxのはanyで、@types/redux-actionsはstring。これが双方同じ型かどちらかのtypeプロパティ定義がなければ問題にならない。。reduxの方がanyで個別ならOKだからTypeScriptの将来バージョンでは通るようになるかもしれないけど、今の所はダメ。前記事の「The hidden power of Jest matchers」とメタで似たような問題ながら、TypeScriptのほうでは融通きかない。

調べるきっかけとなった私のコードは次のReduxのMiddlewareファクトリです。

import { Middleware, Action } from 'redux';
import { push } from 'react-router-redux';

type MoveToPayload = { title: string, vol: string, page: number };

export function moveToMiddleware(): Middleware {
    // redux-actions仕様(=Flux Standard Actions)をDuck Typing
    interface FSA<Payload> extends Action {
        payload?: Payload;
    }
    // reduxのActionとredux-actionsのAction<Payload>に互換性がないからこじあける
    return ({ dispatch }) => next => <A extends FSA<MoveToPayload>>(action: A) => {
        // redux-actionsのAction<Payload>だとnextに渡せない
        next(action);
        if (action.type === 'MOVE_TO' && action.payload) {
            // reduxのActionだとpayloadが受けられない
            const { payload: { title, vol, page } } = action;
            dispatch(push(`/book/${title}/${vol}/${page}`));
        }
        return action;
    };
}

Javaと違ってTypeScriptは型があってもDuck Typingなんで外形が整ってれば同じ型とみなしてくれるから、型をあわせにいきます。型をあわせるためのinterfaceは関数スコープで宣言してみました。通るし、動くけどねえ。こういうものなのかな?まだまだTypeScriptの言語仕様に不案内なので解決方法が怪しい。