Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

.eslintrcを別フォルダに移す

ルートディレクトリにコンフィグファイルがちらばっていると気になりませんか?僕はとても気になります。そこで、設定ファイルを別ディレクトリへ移す方法をご紹介します。

package.jsonに追記するだけで良い

たとえば.config/eslintrc.jsへ移動させたとします。そしたら、以下をpackage.jsonへ記述します。

{
  ...
  "eslintConfig": {
    "extends": "./.config/eslintrc.js"
  },
  ...
}

基本的にはこれだけでOKです。

原理

extendsをつかってファイルを指定すると、そのファイルに書かれた設定が読み込まれます。今回でいえば.config/eslintrc.jsを使うように設定してあるので、これまでと同様に扱うことができます。

上の設定はエディタのプラグインともうまく協調します。また、すでに書いたnpm-scriptsのeslint部分をいじる必要もありません。

Vue3に書き直してみる

Vue2からVue3へ書き直す際に情報が不足していて困ったのでまとめておきます。

公式ドキュメント

まだversion3の公式ドキュメントは揃っていません。マージされたPRRFCを見るしかない状況です。

migrate from v2 to v3

※「これだけ押さえれば7割くらいは移行できるだろう」くらいで網羅的には書けておりません

  1. new Vueによるインスタンス生成はcreateApp()へと置き換え
  2. setupメソッドの引数の型推論をしたい場合はSFCのexportするオブジェクトをdefineComponentでラップ
  3. new Vueのdataやmethodsはinject()app.provide()を使ってグローバルステートで置き換え React Context APIのようなもので、書き方もだいたい同じ。
  4. watchやdata、computed、mountedなどほとんどのプロパティは消えてsetupへ集約
  5. vue-template-compilerの代わりに@vue/compiler-sfcを使う
  6. shims-vue.d.ts(SFCの型定義ファイル)の書き方が変わる
  7. render関数の引数として渡されていたh関数はグローバルインポート

インストール

バージョンは2020年6月19日現在のものです。

  • vue@3.0.0-beta.15

SFCで書きたい、webpackを使いたいのであれば以下も必要です。

  • vue-loader@16.0.0
  • @vue/compiler-sfc@3.0.0-beta.15
  • webpack, webpack-cli

書き方

大きく異なる点がいくつかあります

  • composition APIが使える
  • webpackのalias設定がいらなくなる
  • new Vueが消える
  • Vueオブジェクトに生えていたAPIがnamed exportsオンリーになる
  • TypeScriptとの親和性が高くなる

composition APIが使える

まずこれがかなり大きいです。このお陰でVueへもTypeScriptを導入しやすくなりました。this関連でひどい目に合わずに済むのは大変嬉しいです。

また、書き方もかなり楽になりました。ロジックを外へ抽出できるようになった点も嬉しいです。ReactのHooksとできることはほぼ同じですが、かなりVueらしいやり方に収まった感覚があり、とても好きです。

webpackのalias設定がいらなくなる

new Vueの代わりにcreateAppというメソッドを使ってマウントするようになりました。そのため、vue/dist/vue.esm.jsへエイリアスを張らなくて良くなります。逆に、Vue2の頃のaliasがwebpack.config.jsに残っているとビルドが失敗します。

createApp

こいつが結構やっかいです。従来のやりかたと所々異なっています。

rfcs/0009-global-api-change.md at 9f18645a700f54e7d9a4e3b53046d48791e31459 · vuejs/rfcs · GitHub

上記リンク先を見ていただけばわかりますが、ほとんどは同じです。しかし、たとえば下のようなケースでは若干変わってきます。

// v2
new Vue({
  render: (h) =>
    h(MyComponent, {
      props: { prop1, prop2 },
    })
})
// v3
const app = createApp(MyComponent, { prop1, prop2 });

僕はかなり泥沼にハマりました。

defineComponentについて

setupのpropで型推論を働かせたいときにはdefineComponentでラップする必要があります。逆に、setupでpropを参照しないのならdefineComponentで囲む必要はなく、従来のSFCの書き方で十分です。

公式にも書いてありますが、defineComponent自体は受け取ったオブジェクトをただ返すだけの関数で、型推論以外では1ミリも役に立ちません。

Note that implementation-wise defineComponent does nothing - it simply returns the object passed to it. However, in terms of typing, the returned value has a synthetic type of a constructor for manual render function, TSX and IDE tooling support. This mismatch is an intentional trade-off.

JSXについて

renderのなかでJSXを使えるようになりました(今まではh関数を使ったcreateElement的な書き方しかできなかった)。これでvueファイルの中の<template>を消してJSオンリーでかけるようになりました。

Global APIのnamed exports化

tree-shakingのための変更です。Vue以下に生えていたVue.nextTickなどはnextTick関数としてexportされるようになります。Vue.nextTickは使えなくなります。このように、Vue3には後方互換性のない変更があるので注意が必要です。

SFCの型定義が変わる

new Vueによるインスタンス生成から変わった影響で、SFCの型定義もdefineComponentを使うものへ変わりました。

declare module "*.vue" {
  import { defineComponent } from "vue";
  const component: ReturnType<typeof defineComponent>;
  export default component;
}

従来の型定義はcreateAppの引数として使えないので注意してください。

参考: vue-next(Vue.js 3.0 wip)+ TypeScript + webpackでの開発環境を構築する - Qiita

TypeScriptで配列からnullを取り除く

結論

const a = [1, 2, 3, null];
// nullとundefinedをはじく
const filtered = a.filter(<T>(n: T): n is NonNullable<T> => n != null);
// nullだけはじく
const filtered2 = a.filter(<T>(n: T): n is Exclude<T, null> => n !== null);

nullを除いた配列の型

nullを許容する配列があったとします。この配列にfilterをかけてnullをはじくコードは以下のようになります。

const a = [1, 2, 3, null];
const filtered = a.filter(n => n != null);

しかし、これだけではfilteredの型は(number | null)[]になります。filteredの型をnumber[]型にするにはasかisを使います。

const filtered = a.filter(n => n != null) as number[];
const filtered = a.filter((n): n is number => n != null);

これだけでもある程度は十分です。しかし、オブジェクトの配列であった場合、これだけでは不十分な場合があります。

type Obj = {
  hoge: string;
  fuga: number;
};
type ExObj = Obj & { piyo: boolean };
const objs: (Obj | ExObj | null)[] = [
  { hoge: "hoge", fuga: 3 },
  { hoge: "hoge", fuga: 3, piyo: true },
  null,
];
const filtered = objs.filter((n): n is Obj => n != null); // Obj[]型になって情報が失われる!

というわけで、もっときちんと型をつけましょう。

コード解説

const a = [1, 2, 3, null];
const filtered = a.filter(<T>(n: T): n is Exclude<T, null> => n != null);

ExcludeはUnion型Tを受け取り、TからUを除いたUnion型を返します。たとえばExclude<string | number | null, null>string | numberです。NonNullable<t>はTからnullとundefinedを除いた型を返します。

Exclude、NonNullableを使っているため、コードの意味もnullを除いたUnion型の手書きよりハッキリしています。

参考

参考というか、このページをもとに改良したのがこの記事です。

lower_bound、upper_boundで個数を数える

条件 コード
xより大きい v.end() - upper_bound(v.begin(), v.end(), x)
x以上 v.end() - lower_bound(v.begin(), v.end(), x)
xである upper_bound(v.begin(), v.end(), x) - lower_bound(v.begin(), v.end(), x)
x以下 upper_bound(v.begin(), v.end(), x) - v.begin()
x未満 lower_bound(v.begin(), v.end(), x) - v.begin()

書いておいてなんですが、「xより小さい」個数と「ちょうどxである」個数が求まれば、引き算と足し算で残りは求められるので表にするほどではないですね。

{}は簡易Symbolにできる

Symbolの使い方として、たとえば独自の特殊値を持ちたいと言うものがあります。

// myFind: 配列と関数を受け取り、
// 関数を満たす要素が見つかればその要素、
// 見つからなければnullを返す
const myFind = (arr, f) => {
    for (let i = 0; i < arr.length; i++) {
        if (f[arr[i]])
            return arr[i];
    }
    return null; // 見つからなかった
};
const res = myFind(arr, n => n % 2 === 0);
// ではnullを探したいときはどうする?
myFind(arr, n => n === null); // nullが見つかったのかどうかわからない

このようなケースではSymbolが有効です。たとえばNOT_FOUND = Symbol('not found')として、見つからなければこれを返すようにすれば先程の関数は完璧になります。

const NOT_FOUND = Symbol('not found');
const myFind2 = (arr, f) => {
    for (let i = 0; i < arr.length; i++) {
        if (f[arr[i]])
            return arr[i];
    }
    return NOT_FOUND;
};
const res = myFind2([1, 2, 3], n => n === null);
assert(res === NOT_FOUND);

実はこのとき、Symbolの代わりに単にNOT_FOUND = {}としても同等のことができます。なぜなら{} !== {}だからです。

もちろん細かいところは全然違っているので完全には代用できませんが、ES5でも使えるテクなので覚えておいて損はないと思います。

const NOT_FOUND = {};
const myFind3 = (arr, f) => {
    for (let i = 0; i < arr.length; i++) {
        if (f[arr[i]])
            return arr[i];
    }
    return NOT_FOUND;
};
const res = myFind3([1, 2, 3], n => n === null);
assert(res !== {});
assert(res === NOT_FOUND);

ちなみに

実は先程のNOT_FOUNDを使う手法は厳密にはまだ不十分です。なぜなら、NOT_FOUNDを探したくなったときにnullのときと同じ問題が起こるからです。

厳格にするならばmyFindの返り値は以下のようになります。

myFind(arr, n => n % 2 === 0);
// 見つかった場合 {status: 'found', value: found_value}
// 見つからなかった場合 {status: 'not found'}

とはいうものの、実際には利便性と厳密さをトレードオフしてNOT_FOUNDで実装することも多いです。

ちなみにHaskellではこういうときにMaybeが使われます。とても便利です。JavaScriptにもパターンマッチがあればMaybeでガリガリと書けるのですが…