Panda Noir

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

npm publishの練習をするならverdaccio + docker

みなさん、npm publish、怖くないですか???

npmでライブラリ公開したときに踏みつけた地雷7つ - Panda Noir

npmでアップデートするときに踏みがちな3つの落とし穴 - Panda Noir

僕はもうトラウマだらけです。怖いです。しかし、いつかは克服しなければいけません。では、どうすればいいのでしょうか?

失敗しても良い環境を作ればよいのです。

npm publish はなぜ怖いのか?

npm publish は npm にパッケージを公開するコマンドです。パッケージの更新も同じコマンドでできます。

このコマンドが恐ろしいのは「失敗してもやり直せない」点です。バージョンアップはできるのですが、過去のバージョンの内容を変更することはできません。同じバージョンのはずなのに内容が異なると利用者からすればとんでもないことですから、当然といえば当然です。

しかし、一度公開すると二度とやり直せないのは恐ろしいことです。実際、上の記事で書いたように僕は何度も失敗してしまいました。おかげで npm publish に恐怖しております。

じゃあ npm registry を作ろう

ならば、練習環境を整えれば良いのです。いきなり本番環境にあげるから失敗するのです。開発環境を作りましょう。

やりかたはとても簡単です。Verdaccio というレジストリを Docker 上に起動するだけです。

$ docker run -d -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio
$ npm adduser --registry http://localhost:4873
$ npm publish --registry http://localhost:4873

これだけで使い捨てのnpm registryができます。失敗したらコンテナを破棄してもう一度立ち上げれば良いです。なんと素敵なのでしょうか。

あとはたくさん練習するだけですね!!!みなさんもいっぱい練習しましょう。

esbuild なら React の playground が5秒で出来る!!!!!

esbuild はプラグインなしで JSX・TSXをコンパイルできるから、React のプレイグラウンドがすぐ作れる!!

$ npm init --yes
$ npm i esbuild react react-dom
$ esbuild src/main.tsx --bundle '--define:process.env.NODE_ENV="development"' --outfile=out.js

型チェックが不要なら typescript のインストールすらいらない!!!!tsconfig.json なんて要らなかったんだ!!!

import React, {render} from 'preact/compat';

interface Props {
  name: string;
}
const App: React.FC<Props> = ({name}: Props) => <h1>Hello {name}</h1>;

render(<App/>, document.querySelector('#main'));

webpack も typescript もナシで TSX がコンパイルできる!!!!!!!すごいだろ。しかも早いんだぜ。すげえだろ。

ただし: 結局 Next.js が欲しくなる

まあ、作ってるうちに結局 ESLint も Prettier も欲しくなってくるし、 型チェックもほしくなる。watch すら簡単にできないので面倒。

はじめから Next.js で作るほうが結局は楽なので、playground と割り切るとき以外 esbuild は使わないほうが吉。

ただ、React の機能を試す playground がほしい時は本当にすぐ完成するので最高。

React.forwardRef でジェネリクスを使いたい

たとえば、以下のようなジェネリクスを使ったコンポーネントを考えます。

<Component<string> prop1="string" prop2="string" ref={ref} />

Component の prop1 と prop2 は同じ T 型とします(上の例では T は string)。

ref がなければカンタンに実装できる

ref さえなければ簡単に実装できます。

type Props<T> = {
  prop1: T;
  prop2: T;
};
const Component = <T,>({ prop1, prop2 }: Props<T>) => (
  <ul>
    <li>prop1: {JSON.stringify(prop1)}</li>
    <li>prop2: {JSON.stringify(prop2)}</li>
  </ul>
);

<Component prop1="foo" prop2="bar" /> // 通る
<Component prop1="foo" prop2={42} /> // ちゃんと型エラーになる

codesandbox

シンプルですね。

forwardRef が挟まるとジェネリクスが消える

forwardRef でラップするとジェネリクスが効かなくなります。

const ComponentWithRef = React.forwardRef(Component);

<ComponentWithRef prop1="foo" prop2={42} ref={ref}/>; // 型エラーになってくれない…

codesandbox

forwardRef が返すコンポーネントの型を指定する

これを回避するために、自分で forwardRef が返すコンポーネントの型を指定します。

type Props<T> = {
  prop1: T;
  prop2: T;
};
const Component = <T,>({ prop1, prop2 }: Props<T>) => (
  <ul>
    <li>prop1: {JSON.stringify(prop1)}</li>
    <li>prop2: {JSON.stringify(prop2)}</li>
  </ul>
);
const ComponentWithRef: <T,>(
  props: Props<T> & { ref: Ref<unknown> }
) => JSX.Element | null = forwardRef(Component);

codesandbox

おまけ: React.memo の場合

上の手法は React.memo でも使えますが、React.memo の場合は単に typeof を使う方がカンタンです。

const MemoizedComponent: typeof ComponentWithRef = React.memo(ComponentWithRef);

<MemoizedComponent prop1="foo" prop2={42} ref={ref} />; // きちんと型エラーになる

codesandbox

forwardRef と異なり React.memo でラップする前後で型は変わらないためうまく動きます。

Record & Tuple Proposal がすごい話

github.com

このプロポーザルの話です(2020年8月現在Stage2)

どういうプロポーザルか?

Record と Tuple は、簡単にいうとプリミティブ的なオブジェクトと配列です。どういうことかというと、次のようなことができるようになります。

[1, 2, 3] !== [1, 2, 3] // 配列の場合は等価じゃないけど
#[1, 2, 3] === #[1, 2, 3] // Tuple なら等価!!!

つまり、配列同士の比較を定数時間で行えるようになります

きちんと言うと、Record、Tupleは代数的データ型です。

オブジェクトと配列について

JavaScript では、文字列や数値などのプリミティブ値はインスタンスが異なるなんてことは起こりません。

1 === 1
"a" === "a"

しかし、配列やオブジェクトは異なります。

[1, 2, 3] !== [1, 2, 3]
{hoge: 42} !== {hoge: 42}

こうなっている理由の1つは、おそらくこれらがミュータブルであることです(確証はありませんが)。イミュータブルであれば、オブジェクトから予めハッシュを計算しておいてハッシュ同士を比較するという単純な等価比較ができますが、ミュータブルではそうもいきません。結局、内部ではプロパティの数だけ時間がかかってしまいます。

また、オブジェクトとして等しいかどうかを知りたいケースというのも実際にあります。深い比較は実装できますが、オブジェクトとして等しいかは実装できないので、JS側で提供する必要があります。

うれしいこと

では、オブジェクト同士の深い比較ができるようになったから、何が嬉しいのでしょうか?その答えの1つが React のメモ化です。

React ではメモを使い回すか判断するために、プロパティが変化したかどうか調べています。

// このようなことをしている
isEqual({prop1: 1, prop2: 2, prop3: 3}, {prop1: 100, prop2: 2, prop3: 3})

もちろんこの判定は定数時間でできません。そのため比較のコストとメモ化による恩恵のどちらが大きいか、検討する必要がありました。しかし、Tuple が導入されれば何も迷うことはありません。定数時間で判定できれば、今までのデメリットがなくなります。

また、hooks の依存関係のチェックでも同様の仕組みが使われています。こちらは必ず書く必要があるため、パフォーマンスが向上します。

制限について

Record と Tuple にはいくつか制限があります。

  • Immutable である
  • primitive値とRecord、Tupleのみで構成しなければならない

Immutableである

これはむしろ嬉しい制約です。オブジェクトを渡しても変更されることがないので、影響範囲が小さくなってデバッグが非常に楽になります。もしmutableだとオブジェクトがどこかで変更されていないかチェックする必要があり、非常に大変です。

// オブジェクトがミュータブルだと、調べる範囲が広くなりやすい
const obj1 = {};
func(obj1);
func2(obj1);
func3(obj1);

assert(obj1.hoge === 42); // これは意図してない変化なので、どこで変化したか調べる必要がある

primitive値とRecord、Tupleのみで構成しなければならない

これは2つがどちらも代数的データ型なので当然の結論です。といっても、実際のユースケースではそこまで問題にならないことがほとんどだと思います。そもそも、今のオブジェクトと配列が廃止になるわけではないので使い分けることになると思います。普段はほぼRecordとTupleを使い、できないケースで初めてオブジェクトと配列を使う形です。

// 以下は invalid な Record と Tuple インスタンス
const invalidTuple = #[new Map(), new MyClass(), new Date()]; // ダメ
const invalidRecord = #{
    map: new Map(),
    instance: new MyClass(),
    date: new Date(),
}; // ダメ

ちょうど今の const と let のような形に収まると思います。

めんどうくさがりのためのコンマ演算子を使ったデバッグ術

,演算子(コンマ演算子)を利用したデバッグ方法を紹介します。

返り値をすり替える

たとえば、ある関数の返り値を固定したくなる場面はよくあります。そういうとき、下のように書いてしまうとLinterに怒られがちです。

const isABTarget = () => {
  return true; // デバッグ用
  return targetList.includes(userId);
};

こんなときはコンマ演算子の出番です。

const isABTarget = () => {
  return targetList.includes(userId), true;
};

コンマ演算子は左辺の項を無視して右辺を返します。つまり、 targetList.includes(userId) , true === true です。タイプ数も少ない上、Linterに怒られません。

console.log を挟む

たとえばReactのFCのテストをしたいとき、いちいち波括弧を開いてconsole.logを差し込んでいませんか?

const Component = ({name}) => <h1>Hello {name}!</h1>;

// こうしがち
const Component = ({name}) => {
  console.log(name);
  return <h1>Hello {name}!</h1>;
};

しかし、タイプ数がとんでもなく多くてめんどうです。そこで、コンマ演算子を使います。

const Component = ({name}) => (console.log(name), <h1>Hello {name}!</h1>);

意味としては全く変わっておりません。

注意点としては、この方法が使えるのは式を挿入したい時のみということです。たとえば以下のようなことはできません。

let first = true;
const Component = ({name}) => (if (first) { console.log(name); first = false }, <h1>Hello {name}!</h1>);

この例くらいなら頑張って式に落とし込めなくはないですが、そこまでするなら素直に波括弧で開きましょう。