🧊

続・React x MobXな趣味プロダクトをTypeScriptでリライトした

React x MobXな趣味プロダクトをTypeScriptでリライトしようとした - console.lealog();

という記事を書いてから9ヶ月が経った・・・。

ただ今回は「リライトしようとしてた」ではなく、今度こそついに「リライトした」ので、その過程やらハマりどころをメモっておく。

前回の記事はもう古くなってると思うので、読まなくていいですw

どんなプロダクトなの

GitHub - leader22/mmss-client

  • 個人で借りてるVPSリッピングしたmp3をバックアップしてる(たぶん300GBくらいある)
  • それをよしなにプレイリスト化したJSONファイルで動くオレオレiTunes
  • 機能は少ないけど、なんだかんだ毎日使ってる

なので、TodoAppを小規模というなら、中の下くらいの規模。

TSでリライトしようと思った動機は、単に時間があったのと、Reactが恋しくなったから・・。

depsは変わらず

"mobx": "^5.5.2",
"mobx-react": "^5.3.6",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"styled-components": "^4.0.2"

強いて言うなら`post-css`をやめて、`styled-components`にした。
やっぱすべてをJS界で完結できるのはすごい楽。

`@babel/preset-typescript`

`ts-loader`も`awesome-typescript-loader`も使わずに、Flow感覚でTypeScriptが使えると噂のアレです。

厳密にはTypeScriptの全機能が使えるわけではないけども、一般的な型付けはできるし、コンパイルもしないからビルドも早いし最高!

まあこの規模ならコンパイルしたとて・・って感じとは思うけど、いよいよ個人的にFlowを使う理由がなくなったなー。

Babelはほんとこれだけのために利用してて、あとはいつもどおりWebpackでポン。

ハマったシリーズ

コンパイルしないTypeScriptでコンパイルエラーになるのばっかなので、ケアレスミスな可能性も大いにあり。なんかわかったらぜひ教えてください。

TypeScript と styled-components と Ref

問題のコードがこちら。

class Audio extends React.Component<Props> {
  elRef: React.RefObject<HTMLAudioElement>;

  constructor(props: Props) {
    super(props);

    this.elRef = React.createRef();
  }

  render() {
    const { src, onEnded } = this.props;

    return (
      <Wrap
        ref={this.elRef} // ココが型エラーになる
        autoPlay
        controls
        controlsList="nodownload"
        src={src}
        onEnded={onEnded}
      />
    );
  }

  // ...
}

const Wrap = styled.audio`
  width: 100%;
  vertical-align: top;
`;

コメントにもあるように`ref`を取ってるところでコンパイルエラーになる。

Type 'RefObject<HTMLAudioElement>' is not assignable to type 'string | (string & RefObject<HTMLAudioElement>) | (string & ((instance: HTMLAudioElement | null) => any)) | (((instance: Component<ThemedOuterStyledProps<DetailedHTMLProps<AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>, any>, any, any> | null) => any) & string) | ... 5 more ... | undefined'.
  Type 'RefObject<HTMLAudioElement>' is not assignable to type 'RefObject<Component<ThemedOuterStyledProps<DetailedHTMLProps<AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>, any>, any, any>> & ((instance: HTMLAudioElement | null) => any)'.
    Type 'RefObject<HTMLAudioElement>' is not assignable to type 'RefObject<Component<ThemedOuterStyledProps<DetailedHTMLProps<AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>, any>, any, any>>'.
      Type 'HTMLAudioElement' is not assignable to type 'Component<ThemedOuterStyledProps<DetailedHTMLProps<AudioHTMLAttributes<HTMLAudioElement>, HTMLAudioElement>, any>, any, any>'.
        Property 'setState' is missing in type 'HTMLAudioElement'.

使い方はたぶんあってると思うので、`styled-components`の型がおかしいとは思うけど、よくわからん。

styled-components: Advanced Usage

ワークアラウンドは、`Wrap`じゃなくて`audio`をそのまま使って、CSSを`inherit`とかでごまかす。

TypeScript と mobx(とmobx-react)

2つあります。

`ObservableArray` の初期化
import { decorate, observable, IObservableArray } from 'mobx';

import { Song } from '../../../shared/typings/mmss';

class Finder {
  songs: IObservableArray<Song>;

  constructor() {
    this.songs = []; // ココが型エラーになる
  }

  initSongs(songs: Song[]) {
    this.songs.replace(songs);
  }
}

decorate(Finder, {
  songs: observable.shallow,
});
export default Finder;

エラーはこう。

Type 'never[]' is not assignable to type 'IObservableArray<Song>'.
  Property 'spliceWithArray' is missing in type 'never[]'.

空配列が`never`の配列になっちゃう。

`strictNullChecks: true`なせいでこうなるっぽい記事を見かけたけど、そう言われてもなーという。

ワークアラウンドは、問題の行を次のように。

// this.songs = [];
this.songs = [] as unknown[] as IObservableArray<Song>;

なんだかなー。

`private` と `decorate()` の組み合わせ

これも謎い。

import { decorate, observable, computed } from 'mobx';

class Ui {
  private isHoverPlayer: boolean;
  private isHoverPlaylist: boolean;

  constructor() {
    this.isHoverPlayer = false;
    this.isHoverPlaylist = false;
  }

  get isPlaylistShown(): boolean {
    return this.isHoverPlayer || this.isHoverPlaylist;
  }

}

decorate(Ui, {
  isHoverPlayer: observable, // ココが型エラーになる
  isHoverPlaylist: observable,
  isPlaylistShown: computed,
});
export default Ui;

エラーこちら。

Argument of type '{ isHoverPlayer: IObservableFactory & IObservableFactories & { enhancer: IEnhancer<any>; }; isHoverPlaylist: IObservableFactory & IObservableFactories & { enhancer: IEnhancer<any>; }; isPlaylistShown: IComputed; }' is not assignable to parameter of type '{ prototype?: MethodDecorator | PropertyDecorator | MethodDecorator[] | PropertyDecorator[] | undefined; }'.
  Object literal may only specify known properties, and 'isHoverPlayer' does not exist in type '{ prototype?: MethodDecorator | PropertyDecorator | MethodDecorator[] | PropertyDecorator[] | undefined; }'.

ワークアラウンドは、`private`をやめるだけ。

TypeScriptの問題なのか、MobX側の型の問題なのかもわからず途方に暮れてます。(実害はないけど)

原因が自分の中ではっきりしてないのでどれもIssue立てたりはしてない。

`inject()` したPropsが見えない

これは前回の記事でも書いたのと同じ。
内容は割愛するけども、何も変わってなかったということをお伝えしたい。

思うところ

MobXの使い勝手自体は特に変わりなく、これくらいの規模ならほんとにフィットしてて、必要最低限のコードでシュッと書けて良い感じかなーと思う。

まあ謎の型エラーをこうも踏んでしまって、次はなんか別のにしようかなという気持ちがないといえば嘘になるけど・・。

React Hooksでいろいろ変わるかもしれないし、大して変わらないかもしれんけど、まあぼちぼちやってこ。