import typeならallowImportingTsExtensionsは不要

import { foo } from "./foo.ts";

のように .ts を付けてimportするためにはtsconfig.jsonでallowImportingTsExtensionsがtrueになっている必要がある。しかしこのオプションはnoEmitも同時にtrueにしなければならない。その理由はuhyo氏が解説している。トランスパイルしたら.tsファイルじゃなくなるのでimportできなくなって意味ない(そこの辻褄合わせまでtscはやりません)のでトランスパイルしないでくださいということ。

https://zenn.dev/uhyo/articles/rewrite-relative-import-extensions-read-before-use#--allowimportingtsextensions-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3

じゃあトランスパイル後に消えていいimport、つまりimport typeならよくね?と思って調べたら、実際そうだった。

// allowImportingTsExtensionsがfalseでもエラーにならない
import type { foo } from "./foo.ts";

https://github.com/microsoft/TypeScript/pull/54746

これに気づいたのは、vibe codingで軽めに作っているアプリケーションにテストを追加しようとしたときだ。現状nodeのビルトインテストフレームワークと--experimental-strip-typesを使うとjestやらvitestやらがなくてもそれっぽいテストが作れる(jestのためにトランスパイルとか、結構複雑だったよね)。

https://blog.koh.dev/2024-10-23-nodejs-builtin-test-typescript/

しかしnodeとして実行するならimportには拡張子が必要であり

https://zenn.dev/mizchi/articles/experimental-node-typescript#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E8%A7%A3%E6%B1%BA

拡張子をつけると今度は「allowImportingTsExtensionsをtrueに旋回!」と怒られる。うーん、このルールの意図を考えればimport typeにすれば通るんじゃね?と思ったら通ったという次第。import typeにできる箇所で良かった。というかそうでなければそもそもtscだけではビルドできなかったんだけどな。

nodeのドキュメントにもimport typeはstripすると読める説明が書いてあった。

https://nodejs.org/api/typescript.html#importing-types-without-type-keyword

Due to the nature of type stripping, the type keyword is necessary to correctly strip type imports.

TypeScriptとの微妙な距離感が面白かった。

https://nodejs.org/api/typescript.html#importing-types-without-type-keyword

As in JavaScript files, file extensions are mandatory in import statements and import() expressions: import './file.ts', not import './file'.

The tsconfig.json option allowImportingTsExtensions will allow the TypeScript compiler tsc to type-check files with import specifiers that include the .ts extension.

あくまで拡張子付きのimportという文法が先にあり、それをTypeScriptが許可するかはオプション次第ですね〜というスタンスが感じられ、なるほど言語を作っている方からはそういう見方になるんだなあと思った。

20250420 回転寿司/gpt-4.1-miniの印象/prismaでgroupBy

遅めに起きて布団カバーを洗濯した。また冬が終わったので毛布類をコインランドリーで洗った。待つ間にくら寿司で豪遊(1100円)。

回転寿司

回転寿司というのは、極みである。食事の進行中に1品ずつ注文を受けてオンデマンドで調理される自由度の高さ、それを支えるweb注文とコンベヤ輸送という温かみの欠片もない高度な技術によるオペレーション、射幸心を煽るびっくらぽん。揚げ物スイーツラーメンなんでもありの無文化性。そんなに美味しくない寿司、腹に貯まる米による満腹感。誰も触らないせいで回転レーンで干からびていく寿司(そしてそのせいで一層誰も触らなくなる)。

食べる分量が空腹である入店時には決まらないという性質上食べ過ぎにくいというのはなかなか良いところだと思う。

洗いたての毛布を持ち帰って神の昼寝。起きたら友人が麻雀で大負けしていた。

gpt-4.1-miniの印象

またいろんなLLMを試しながら個人開発。これ自分じゃメンドクセぇ〜って思うところはやっぱりLLMにも任せられないね。今日の感触としてはこんな感じ。

  • gemini2.5: やたらと作業ログをコメントで残す。やめろと言ってもやめない。diffツールの使い方が下手で何度もやり直し金ばかりかかる。触るなと言ったところを触る。
  • gpt-4.1-mini: 頭も記憶力も悪いが時間をかけて誘導すれば一応仕事はできる

僕は安くてそこそこ使えるやつに興味があり、その点ではgpt-4.1-miniは良い。claudeはお高くて使いづらいんだけど優秀であるということを痛感。gemini2.5は高いし評判も良かったけど使ってみたらそれほどでもなかった。

prismaでgroupBy

具体的にやった作業はprismaのクエリいじり。アニメの作品(work)とエピソード(episode)が一対多対応であるという前提で、ある条件を満たすようなepisodeを2つ以上持つworkを抽出したい。生SQL(を使えるprisma API)だと

const works = await prisma.$queryRaw`
  SELECT w.*
  FROM work w
  JOIN episode e ON e.work_id = w.id
  WHERE e.some_condition = true
  GROUP BY w.id
  HAVING COUNT(e.id) >= 2
`;

のようにwhere→group by→havingの流れで2回絞り込みを行うことで実現するらしいのだが、これをprismaに持っていくと

const works = await prisma.episode.groupBy({
  by: ['workId'],
  where: {
    some_condition: true,
  },
  _count: {
    workId: true,
  },
  having: {
    workId: {
      _count: {
        gte: 2,
      },
    },
  },
});

となり、返り値の型が { _count: { workId: number }, workId: number } になる。つまり SELECT w.* が再現されずworkIdしか取れない。

既知のissueとしてはこの辺りが近い話に思われるが、いずれも対応される雰囲気がない。
https://github.com/prisma/prisma/issues/24816
https://github.com/prisma/prisma/discussions/6517

まあしないだろうなという感覚もわかる。prismaはそもそも様々なデータベースを隠蔽する抽象化の役割も持っており、各データベースのある程度細かい機能に逐一対応するのは無理だ。TypeScriptとの堅固な統合が持ち味であることを考えれば難易度は一層高い。

ある程度複雑なクエリ、パフォーマンスチューニングが求められるクエリは生SQLのAPIを使ってくださいよということなのだろう。

20250403 Lerna

項目 内容 得点
起床 9:58 2
散歩 ノー 0
朝食の栄養カバレッジ 3色カバー 100
体操 ノー 0
労働 した・passion: 85点, discipline: 65点() 75
ジム サボった 0
勉強会 参加 100
個人開発 実施 100
あすけん - 72
総合 1日の総合評価 56

個人開発でちょっとした機能を追加したく、deepseek-r1にやらせようと思ったが、一人であれこれ思考を巡らせている間にAPIがエラーになって何もできなかった。gemini-2.0-flash-001に切り替えてみると明らかに返答がバカ速くなり、思考に僕が追いつけなかった。でも出てきた編集内容はトンチンカンで、ダメ出しするとまた高速で思考し始めたが、結局何の成果も出せないままRoo Codeに「このタスクはgeminiくんには難しすぎたんじゃない?」と言われて終わった。やはり安いLLMはそれなりだ。

腰痛・膝痛は改善した。

JavaScriptのモノレポ管理ツールでlernaってあるじゃないですか。多頭のドラゴンをロゴにしてるやつ。これなんでlernaなんだろうと思ってChatGPTに聞いてみたらlernaは地名でレルネーのヒュドラというギリシャ神話の怪物をモチーフにし、ヒュドラは名前被りが多いのでlernaの方を取ったのではないか、という話だった。なるほどなあ。

断片的にはメンバーのこういう言及もある。
https://github.com/lerna/lerna/issues/1858#issuecomment-452782408

現実のlernaはこんな感じの草原と丘のある村だ。

風景をGeogusserの友人に送りつけてここどこだ?してみたらギリシャなのは一発で当てた。

YouTubeの広告ブロッカーブロッカーの研究

自由研究 としてYouTubeが広告ブロッカーを検知する仕組みを調べていた。有名な広告ブロッカーを狙い撃ちにしているのかと思いきや、僕が 研究用 に作った簡易的なブロッカーにもちゃんと反応していた。

https://www.youtube.com/s/player/5ae7d525/player_ias.vflset/ja_JP/base.js より抜粋

    g.E.onClick = function(k) {
        k && k.preventDefault();
        var p, t;
        dDL(k, {
            contentCpn: (t = (p = this.api.getVideoData(1)) == null ? void 0 : p.clientPlaybackNonce) != null ? t : ""
        }) === 0 ? this.api.zb("onAbnormalityDetected") : (SA.prototype.onClick.call(this, k),
        this.api.zb("onAdSkip"),
        this.api.onAdUxClicked(this.componentType, this.layoutId))
    }

クリックイベントkをdDLに渡し、その返り値が0ならonAbnormalityDetectedイベントが発行され、その結果広告ブロッカーやめろ画面に遷移する。

    dDL = function(k, p) {
        var t = 1;
        k.isTrusted === !1 && (t = 0);
        a3("ISDSTAT", t);
        qO(t, "i.s_", {
            triggerContext: "sk",
            metadata: p
        });
        return t
    }

dDLの実装を見るとisTrustedがfalseなら0が返る。
https://developer.mozilla.org/ja/docs/Web/API/Event/isTrusted

ユーザーを意図しない操作から守るためにブラウザが提供している機能なので、これを出し抜くのは結構難しい。相当な荒業でやる方法はあるようだが、addEventListenerが実行される前にinjectせねばならずなかなか難易度が高い。
https://qiita.com/i11u/items/0a6d38966c75314bacba

Chrome extensionのdeclarativeNetRequestを利用してbase.jsがダウンロードされるときに該当箇所を書き換えてしまえばいいのではと思ったのだが、preload+service workerが使われている影響かbase.jsのGETを捕捉できなかった。あとはプロキシサーバーを立ててbase.jsだけすり替えるとかできるのかなあ。なんならChromiumをforkしてisTrusted機能を殺してしまうのもありかもしれない。Chrome拡張という配布のしやすい形を諦めるなら選択肢はいろいろある。

あくまで 研究 ですからね。私欲のために実行しませんよ。こういう研究をしていると広告が流れてくれた方が都合がいいのだが、そう思っているときに限って意外なほどに流れない。こんなに頻度低かったかな…?と思ってしまい、それなら別にいいやとなってしまった。逆物欲センサー(だいぶ昔の言葉になってしまったな)。

webサービスをどう設計するかは作り手の自由だが、それと同時に我々が自分の所有する計算機でどのような計算を行うかも自由だ。つまりはそういうことだ。

こういう漠然とやりたいことはあるけどどうやるべきかもどう書くべきかもわからない状況、雑にCline+Claudeに書かせておいてよくわからないところはCopilotに説明させてあーそういう意味ねと納得したりしている。Clineが大きなスコープでの作業を得意とする一方でCopilotはインライン操作が充実していて細かい部分について質問しやすい作りになっている。LLMの特性と言うよりはプロダクトとUIの特性だね。

20250208 validation

10時頃起床。のんびり個人開発とDota2をして過ごし、夕方に薬をもらいに病院に行った。生活がそこそこ安定しているので睡眠薬を減らすことにした。

その後少し散歩した。中古のPS2が欲しくてありそうな店を少し回ってみたが、あってPS4だった。そりゃそうか。いつも特に関心を持つこともなかった服屋に入ってみたら中が意外と広くて、道から見ると間に数件の別の建物を挟んでもう1つ入口があった、というか別の店だと思っていた2軒が実はつながっていたことを知った。不思議なこともあるものだなあと思いながら建物の全貌を知りたくで側面に回ったら参政党のポスターが貼ってあってうおwとなった。

道道道道道
道■□□■
道■■■■

道から見えるものと実際の建物の構造が全然違うのってロマンあるよね。そもそも建物自体が外観と内装を分離する性質を持っていて、外から見るとただのマンションに見えるけど実は中は超巨大な1部屋かもしれないと考えることがある(大昔にブログに書いた気がしたが見つからなかった)。

それにしても寒い。僕も子供の頃は冬でも薄着だったが今薄着の子供を見ると暖かくしろ〜〜〜!!と思う。寒さか乾燥かにやられてずっと喉が痛い。参った。

帰宅してからは個人開発を再開。Dota2は負けてばっかりなのでまた引退します。バックエンド側でPOSTされるpayloadをvalidationする処理を何度も何度も書いていて、これもうちょっと統一的かつエレガントにならんか…?ということでこの辺を読んでいた

どうしてvalidation書くのにApplicativeとか半群のことわからなきゃいけないんですか(現場猫顔)。でも型の力を徹底的に活かそうとしていくとそうなるんだよな。暗号の勉強でもガチ数学が迫ってきているし、自分の力がそういう領域に到達しつつあるということは喜ばしいなと思う。

伸びる芽の数がとても少なくなっている。遅れて他も伸びるかな…?

20250125 子供の活躍

9時頃起床。病院(頭)、薬局、朝マック、ダンス見物、展覧会、昼食、ジム(風呂のみ・相撲観戦)、夕食、夕寝、個人開発、今。

反省

昨日の日記は良くなかった。日記というのは、やることが全部終わって寝るだけの状態でchill…の機運に包まれながら一日のことを振り返り、その出来事のあったときの生のフィーリングとそれを俯瞰する執筆時の冷静さがブレンドされてこそ味わい深くなるし、自分にとっても意味がある。

日記

昨夜は明日が休みなので…と夜ふかししてしまったが、通院があるのでそこそこ早起きは必要で睡眠不足だった。ここ2週間ほどは規則正しく十分な睡眠を取ることに慣れていたので今日はずっと微妙な体調だった。

処方箋を持ってショッピングモール内の薬局へ。薬局は土曜午後になると高くなるので午前のうちに。でもみんなそう思っていたのか混んでいて40分かかると言われたので、モール内でブラブラして時間を潰す。ふとマクドナルドが目に入って、朝食を食べていなかったのでバーガー1つで朝マックとした。味は普通。しかしマクドナルドはそれしか注文しない人間にすごい量の紙を出してきて(敷くやつ、包むやつ、拭くやつ)アメリカンだ…となった。

薬を受け取り帰ろうとすると、モール内の広場で子どもたちがダンスしていた。近くのスクールらしい。さすがにスクールで本格的に習っているだけあってみんな照れがなく、堂々たる身体表現だった。ダンスを人と合わせるのは音楽より難しそうに見えた。音楽は出ている音さえ合っていれば身体動作がきっちり一致する必要はないのだが、ダンスは拍のポイントで同じポーズをするだけではなく、ポーズからポーズの間の遷移も同じf(t)で表現されなければならないからだ。

帰り道にちょっとした美術館があり、地域の中学の美術部の合同展覧会をやっているとのことだったので見てきた。率直に言うと中学生の絵というのは子供らしい素直さ・独創性がない一方で大人らしいテクニック・思想性もまだ身につけておらず、「うーん、中学生っぽい!」と感じるものが多かった。まあそれはそういうもので、自然なことだと思う。しかし素晴らしい熱意・センス・工夫あるいはトンチを感じる作品もあった。

活力のある子どもたちに刺激を受けて、僕もやっていき!が高まっているのでとりあえず手持ちのRemix製のwebアプリケーションは一通りv3フラグに対応した。Remixはまだ色々進化しようとしていてすごいなあ(職業ソフトウェアエンジニアとしては怖いけど)。Rustで作っているちょっとしたCLIツールもclapなるオーギュメントパーサーを導入していい感じに進化させている。今のところ細かいメモリを扱う処理はなく所有権周りの機能は活用していないが、そういう人間にとってもマクロ機能がとても便利だ。RustのstructはTypeScriptのobject/type/interfaceに比べるとやや取り回しが悪いかなという印象だが(たとえばネストで定義できない)、素のstructにマクロでいろいろな機能を付与していける(たとえばserdeのrenameとかclapのlong/shortとか)のは本当にすごい。Goのstruct tagも面白い機能だなと思ったけどマクロの使用感はその上を行っている。作る方は大変なんだろうけどな〜。

真・三國無双2

ここ数日、『真・三國無双2』の動画を見るのがマイブームになっている。僕が小学生くらいの頃に友達の家でよくやった(僕は所有していない)。24年前に発売されたゲームなのだが、現代まで続くシリーズの礎となった偉大な作品でもある。

大昔のゲームに対する深い知識と研究、そして投稿動画が2本なのに妙に上手い動画編集。何者なんだろう。YouTubeの世界は広いなあ…(そして最近暗号の勉強をしているので、疑似乱数への対処法を見るのは面白い)

豆苗

もう「刈って」いいか…?←刈りました

ReactでdetailsをoutSideClickで閉じるやつ

自作のアニメ視聴管理webサービス watch-duty-manager にこのようなメニューUIがある。ハンバーガーを押すとメニューが開く。これはHTMLのdetails/summaryタグで実現している。

こういうメニューはメニューの外側をクリックすると閉じるのが一般的だ。しかしdetailsタグは標準でそのような挙動をしない(まあメニューにだけ使われるものではないからね)。summary(ハンバーガー部分)をもう一度押すことで閉じることができる。外側クリックで閉じる方法を適当にググってみると

  • detailsが開いているときはcssでsummaryを画面全体を覆うように巨大化させる
  • JavaScriptのクリックイベントを全て拾ってなんとかする

という2つの方針が見つかった。1はなかなか賢いと思うものの、真面目に作り込もうと思うとなかなかバギーな部分もありそうで(特にレイヤー順とか)やめて2をやることにした。

みんな大好き react-use に useClickAway というカスタムフックがある。refで参照されたHTML要素の外側がクリックされたときに指定されたコールバックを実行する。実装としては愚直に2をやっているだけ。

// https://github.com/streamich/react-use/blob/ad33f76dfff7ddb041a9ef74b80656a94affaa80/src/useClickAway.ts#L16-L22

const handler = (event) => {
  const { current: el } = ref;
  el && !el.contains(event.target) && savedCallback.current(event);
};
for (const eventName of events) {
  on(document, eventName, handler);
}

これを使って

function Component() {
  const ref = useRef<HTMLDetailsElement>(null);
  useClickAway(ref, () => {
    if (ref.current && ref.current.open) {
      ref.current.removeAttribute("open");
    }
  })
  return (
    <details ref={ref}>
      <summary>summary</summary>
      details
    </details>
  )
}

てな感じでやれば一応動くものは作れる。作れるのだが、この作りだとdocument内のどこをクリックしても、画面内のこのコンポーネントの数だけ ref.current && ref.current.open の評価が走る。実際のユースケースでは100個くらいの表示はあり得る。まあ実際のところ100個あろうと大したコストではなく、使用感に影響を与える16msには届かないのだが、個人開発だからそういう現実的な損得はおいといて、もっと効率的な作りを追求することにした。

この作りの何が無駄かと言えば同時に開くdetailsは1個しかないのに画面内のdetails全てに対して同じ判定関数を与えていることだ。そこでデフォルトではなにもしない関数を渡しておいて、detailsが開いたときだけuseClickAwayに渡す関数を差し替えることにした。

const noOpCallback = () => {};

const useCloseDetailsOnClickAway = () => {
  const ref = useRef<HTMLDetailsElement>(null);
  const closeCallback = useCallback(() => {
    if (ref.current) {
      ref.current.removeAttribute("open");
    }
  }, [ref.current]);
  const [onClickAway, setOnClickAway] = useState({ f: noOpCallback });
  const onToggle = useCallback<React.ReactEventHandler<HTMLDetailsElement>>(
    (_) => {
      if (ref.current === null) {
        return;
      }
      if (ref.current.open) {
        setOnClickAway({ f: closeCallback });
      } else {
        setOnClickAway({ f: noOpCallback });
      }
    },
    [],
  );
  useClickAway(ref, onClickAway.f);
  return { ref, onToggle };
};

function Component() {
  const { ref, onToggle } = useCloseDetailsOnClickAway();
  return (
    <details ref={ref} onToggle={onToggle}>
      <summary>summary</summary>
      details
    </details>
  )
}

その結果できたカスタムフックがこれだ。detailsのtoggleイベントを監視して、開いたものだけuseClickAwayのコールバック関数をcloseCallbackに差し替えている。

ちなみに数字上の速度差はなかったです。たぶんReactやV8がなんとかしてるんでしょ。

 

20250105 作らされたいもの/RimWorld終盤の資産管理と人生訓

作らされたいもの

作りたいものねぇ〜とか言っていたら昔作ったwebアプリケーションをまだ使ってくれている友人から機能の要望が来て、若干の修正と膨大なリファクタリングが発生していて楽しい。しかし連休の終盤に楽しくて夜ふかししているのは頭が悪い。

昔作ったものだから結局コンフォートゾーンに逆戻りしているということではあるんだけど…まあ何もしないよりはマシかな。TypeScript+prismaでデータベースを複雑にいじくるところで(mysqlだとcreateManyAndReturnが使えません)、fp-tsで型をガチガチに固めているので絶対間違ったプログラムを書けないのだが、だからと言って正しいプログラムが書けるというわけでもなく苦労している。fp-tsの型エラーは原因箇所と遠く離れた場所に出るので何を間違ったのか探すのにコツがいる。好きな思想のライブラリではあるんだけどやっぱり言語機能の支援なしにこういうことをやろうとするとどこかに無理が生じるのよね…。

本日

今日からジムが開いていたので行って全身シバいてきた。少し衰えていたが想定内だ。NHKラジオニュースではUSスチールの買収差し止めが大きく扱われていた。大変だ。昼は久々に餃子の王将に行ってみたが、日高屋よりは美味しいだろうと期待しすぎた結果思ったほどじゃないな…となった。かぼちゃを買って帰宅。

RimWorld終盤の資産管理と人生訓

RimWorldは佳境である。いよいよ宇宙船の研究が一通り完了し、後は地道にウランやらプラスチールやら先進コンポーネントやらをかき集める段階に入った。ここの進め方がなかなか難しい。コロニー経営は軌道に乗っていて金も食料も有り余っているのだが、希少資源はそうそう見つからないので金余りの状態になりやすく、そのせいでゲームシステムによって敵の襲撃が強力になってしまう。かと言って金を捨てるといざ希少資源が見つかったときに買えない。つまりゲームシステムにバレない場所に資金を移動させる必要があるのだ。

一つの方法としてはキャラバンに乗せて適当な人員にコロニー外を徘徊させるというのがある。物理的な資産隠しだ。しかしこれはあまりにもゲームを騙してる感が強くポリシーに反するのでダメ。他の方法としては近隣の他勢力に配っちゃうというのがある。もちろんその資金は消滅するのだが、その分友好度が上昇し、いざというときに助けに来てくれたりキャラバンを派遣してくれたりする。

人を働かせて金を集めるシステムを構築したら次はその金を隠したり配ったりというのは嫌なリアリティがあるが、溜め込むくらいなら人に配ってしまえというのはなかなかポジティブな人生訓ではないだろうか。資産を持つことはそれ自体がリスクである。価値は損耗・変動するし、奪われる可能性もある(現代日本の銀行だって1000万円までしか保証してくれないぞ)。それに対して、ロマンティックに言えば友情、システマティックに言えば互恵性の規範に投資するという道もあるぞということをこのゲームは提示しているのだ(厳密に考えると金銭投げつけは友情なのか?という疑問は新たに浮かんでくるが…)。

ちなみに住人の一人であるElk(39歳独身男性・社交能力1)は性欲が強すぎて人妻を含む周囲の女性に声をかけまくり、当然のごとく振られ続けた結果メンタルを悪化させ暴れだしたので逮捕・収監と相成りました。そのうち復帰はさせようと思うけどもう女性と顔を合わせないように深夜シフト固定かなあ。これはネガティブな人生訓。

Next.jsのReact EssentialsのServer Componentsの説明をオレオレ翻訳しながら読む

これの話

https://nextjs.org/docs/getting-started/react-essentials#server-components

正確さではなく(それならAIでいいよね)、自分の理解に引き寄せて翻訳してみる。

Server Components

Server and Client Components があるとサーバーとクライアントの両側にまたがるアプリケーションを作れます。クライアントサイドのリッチなインタラクティビティと伝統的なサーバーレンダリングのパフォーマンスを組み合わせることができます。

Thinking in Server Components

ReactはUI設計の考え方を変えました。同じようにReact Server Componentsは、サーバーとクライアントの両方を活かしたハイブリッドアプリケーションを作るための新しい考え方です。

Reactはこれまで全てをクライアントサイドでレンダリングしていました(SPAというやつです)。しかし、Server Componentsがあると目的に応じてどこでレンダリングするかを決められます。

たとえば、アプリケーションのpageを考えます。

ページをコンポーネントに分割すると、実は大多数のコンポーネントはインタラクティブではないです。つまりサーバーサイドでServer Componentとしてレンダリングできます。そしてその中にインタラクティブなクライアントコンポーネントを点在させるのです。これはNext.jsのサーバーファーストなアプローチと相性がいいです。

Why Server Components?

Server Componentsのメリットは何?という疑問が浮かぶでしょう。

Server Componentsはサーバーのインフラを活用しやすいです。たとえば、巨大な依存パッケージをクライアントに送信する必要がありません。こうなるとReactはPHPやRoRのよう(テンプレートエンジンのように?)に扱えます。

Server Componentsは初期ページ読み込みが早いです。バンドルサイズが小さくなります。根幹部分のクライアント側ランタイムはキャッシュ可能かつサイズの予測が可能で、アプリケーションが成長しても増えません。追加されるJavaScriptは、Client Componentsが使われたときだけ増えます(この辺あんまりわかってない)。

Next.jsでrouteが読み込まれたとき、初期HTMLがサーバーでレンダリングされます。このHTMLはブラウザで段階的に成長し、クライアントがアプリケーションを引き継ぎ、インタラクティビティが付加されます。このためのランタイムの読み込みは非同期的です。

Server Componentsに楽に移行できるように、App Router内のコンポーネントは全てデフォルトでServer Componentsにします。special filesやcolocated componentsも同様です。だからあなたは何もしなくてもServer Componentsを採用して優れたパフォーマンスを得られます。ここに use client directiveを使うことでClient Componentsをオプトインできます。

Client Components

Client Componentsを使うとクライアント側でのインタラクティビティを付加できます。これはNext.jsではサーバーでのpre-renderingとクライアント側でのhydrationで実現しています。Client ComponentsはこれまでのPage Routerと同じ動きです。

The "use client" directive

use client directiveはServer / Client Componentsの境界線を宣言するものです。

'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

"use client" はサーバーとクライアントのコードの間に配置します。ファイルの一番上、importsよりも上に書きます。"use client" が宣言されると、そのファイルがimportしている全てのモジュール(子コンポーネントなど)はクライアントバンドルの一部と認識されます。

デフォルトはServer Componentsなので、 "use client" 宣言がない限り全てはServer Component moduleに含まれます。

豆知識

  • Server Component moduleに含まれるComponentはサーバーでのみレンダリングされることが保証されます
  • Client Componentはクライアントでレンダリングされるものですが、Next.jsは事前にサーバーでレンダリングすることもあります
  • "use client" はファイルの一番上で宣言しなければなりません
  • "use client" は全てのファイルで宣言する必要はなく、Server Componentsとの境界でだけ宣言すれば十分です(そのファイルがimportしているファイルもクライアントバンドルになるので、"use client" が宣言されたファイルはentry pointとみなすことができます)

おわりに

Server Componentsが何なのか知りたくて調べていたんだけど、なんか概念的な記事しかなくてよくわからなくて、この記事もそうだった。完。でも概念はちょっとわかった。

TypeScriptの引数を分割代入したときの ‘A’ is assignable to the constraint of type ‘T’, but ‘T’ could be instantiated with a different subtype of constraint ‘B’ みたいなエラー

T型の引数を受け取ってそのまま返す関数だが、返り値の型をTにすると型エラーになる。

const f1 = <T extends { user: { id: number } }>({ user, ...rest }: T): T => {
  return {
    user,
    ...rest,
  }
}
Type '{ id: number; } & Omit<T, "id">' is not assignable to type 'T'.
  '{ id: number; } & Omit<T, "id">' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ id: number; }'.(2322)

このエラーメッセージが難しい。 T extends { user: { id: number } } に注目する。extendsだからTはたとえば

type T1 = {
  user: {
    id: number;
    name: string;
  }
}

かもしれない(狭い型)。そうだとしても、関数内でのuserの型は { id: number } だ。nameがあることにはならない。TypeScriptならわかるかと思ったがわからないらしい。

Tに何を渡されようと返り値のuserの型は { id: number } (広い型)なので、それを T["user"] (狭い型)に渡しても要求を満たせないことがある。だから型エラーになる。

という話はここに書いてあるのだが
https://github.com/microsoft/TypeScript/issues/33579#issuecomment-534789400

このように書くと引数の型を維持できる。

const f2 = <T extends { user: { id: number } }>({ user, ...rest }: T): Omit<T, "user"> & { user: T["user"] } => {
  return {
    user,
    ...rest,
  }
}

const b = f2({user:{id: 1, name: "chao", premium: false}})
const p = b.user.premium // アクセス可能

TypeScriptくんは書き方によって推論ができたりできなかったりする。かわいい。