スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

カスタムフックを使いながら、バーチャル背景生成ツールを開発した話

🦓 < こんにちは!

先日、完全なReact初心者が社内用の「バーチャル背景生成ツール」を開発・デプロイしました!

説明するより、みてもらったほうが早いと思うので、完成したものの動画を貼っておきます 👇

シンプルなツールですが、作る過程でさまざまな学びがあり、その中でもカスタムフックについて残そうと思います!

是非読んでいってくれると嬉しいです 🙇‍♂️

使用技術と構成

先にさっと、使用技術の詳細と構成を書きます

  • React Router
  • Chakra UI
  • Vite

ファイル構成

packages/client/src
├─ assets/        — 画像・フォントなど
├─ components/    — 共通Reactコンポーネント
├─ constants/     — 各種定数
├─ hooks/         — カスタムフック
├─ pages/         — 画面コンポーネント
├─ test/          — テスト類
├─ types/         — 型定義
├─ app.tsx        — ルートApp
├─ main.tsx       — DOMエントリ
├─ router.tsx     — ルーティング設定
└─ vite-env.d.ts  — Vite型宣言

ViewとControllerを分ける??

自分で実装してみたり、Copilotくんに頼ったりしながら実装してました。

しかしいざレビューをしてもらう時に、

「見た目のコンポーネントにコントローラー相当の実装を詰め込まない方がいい!」

と言われました。

一体どういうことだ...? となりつつも、教えてもらいました!!

React は厳密な MVC ではないものの、「UI(View)に寄せた部分」と「ロジック(Controller 的な振る舞い)」を意識的に切り分けると読みやすくなるケースが多いとのこと。

ずばり、今回のケースではUIとロジックを分けると書きやすかった、つまりTSXでは表示のみに徹しカスタムフックでコンポーネントのロジックを受け持たせるのがよい構成だったのです。

これだけだとピンとこないので、実例を見てみましょう

分ける前

これを

import { useEffect, useState, type ChangeEvent } from "react"

export function TestComponent() {
    const [inputText, setInputText] = useState<string | null>(null);
    const [response, setResponse] = useState<string | null>(null);

    useEffect(() => {
        // 複雑な処理
        setResponse(`inputは${inputText}だよ!`)
    }, [inputText])

    const onChange = (event: ChangeEvent<HTMLInputElement>) => {
        setInputText(event.currentTarget.value);
    }

    return (
        <div>
            <input type='text' placeholder='Enter some text' onChange={onChange} />
            {response ? <p>{response}</p> : <></>}
        </div>
    )
}

分けた後

こうする!👇 (カスタムフック使用)

import { type ChangeEvent } from "react"
import { useResponder } from "../hooks/useResponder";

export function TestComponent() {
    const { setInputText, response } = useResponder();

    const onChange = (event: ChangeEvent<HTMLInputElement>) => {
        setInputText(event.currentTarget.value);
    }

    return (
        <div>
            <input type='text' placeholder='Enter some text' onChange={onChange} />
            {response ? <p>{response}</p> : <></>}
        </div>
    )
}
import { useEffect, useState } from "react";

export function useResponder() {
    const [inputText, setInputText] = useState<string | null>(null);
    const [response, setResponse] = useState<string | null>(null);

    useEffect(() => {
        if (!inputText) {
            setResponse(null);
            return;
        }
        setResponse(`inputは${inputText}だよ!`)
    }, [inputText])

    return {
        setInputText,
        response
    };
}

分けておくと、見た目のコンポーネントはスッキリ保てます。 特に機能が増えてロジックが膨らんできたときに効果を発揮します 😎

カスタムフックの便利なところ

1. コードの見通しがよくなる/理解しやすくなる

やっぱり、これ大事ですよね。

  • UIを変えてもフックのインターフェースさえ守れば影響範囲を抑えられる。
  • ロジックもフック内に閉じ込めておけば、状態管理の手戻りを減らしやすい。

責務を分けることでUIとロジックの境界が明確になり、余計なバグを減らしながら開発できるのが本当にいいなと感じました 👀

2. ロジック・状態を使いまわせる

一度フックに必要なロジックをしっかり書けば、他のコンポーネントでそのロジックと状態管理を使いまわせる!

例えば、他のコンポーネントで書いた

  • 入力値の設定
  • ファイル生成
  • ダウンロード

というコードがあったとして、それを何度も貼り付けなくていい=保守性が上がる

しかも、コンポーネント側では入力値を設定するだけなので、表示周りのコードの見通しも保ちやすいです。

3. テストがしやすくなる

テストをするときに、全て一つのファイルに詰め込むと、 結果として、ビューのモックを作ったりあらゆる値をセットしないとテストできない!っていうことがありえるかも知れません。

フックにロジックを分離すれば、フック単体にフォーカスしたテストを書きやすくなりますし、最小限のモックで済ませられるケースも増えます。

これもとても大きい恩恵だと思いました

終わりに

今回、いろんな初見の技術を使いつつ、様々なことを学びながら実装ができました!!

これからもどんどん新しい概念や設計の基礎を学び、より良い品質で開発ができるように頑張ります〜

段階的にぼかす UI 表現 〜Web における Progressive Blur の実装と活用〜

こんにちは。ちゅるりです。

今回は、段階的にぼかしていく UI 表現の一例として Progressive Blur(プログレッシブ・ブラー)をご紹介しつつ、実装から実際のデザインへの組み込みまでを包括的に述べていこうと思います。

Progressive Blur とは

概要

タイトルなどでも既に述べてしまっていますが、ひと口で言えば「徐々にぼかし半径が変わっていくぼかし」です。ぼかしのグラデーションとも言えるでしょう。

百聞は一見にしかず、例をご覧いただけるとわかりやすいかと思います。次の図は、Progressive Blur を用いたカード実装の一例です。

Progressive Blur の一例(自サイト slides.itsu.dev より)

このように Progressive Blur は 2 つ以上重なった要素の境界においてその真価を発揮します。例えば iOS18 のコントロールセンターにおいて、ドロワーの引張具合によって背景のぼかし度合いが変化していく部分に Progressive Blur を見ることができます。Progressive Blur は磨りガラスのような、シックでモダンな印象を与えたい場合に有効であることがわかります。

現に海外の UI デザイナーコミュニティで最近まで盛り上がっていた印象があります。 確かに海外のモダンな LP で多用されているイメージがありますね。

Gaussian Blur との違い

Progressive Blur よりも広く使われているぼかし表現の一つに、Gaussian Blur(ガウシアン・ブラー)があります。いわゆる単なるぼかしで、ぼかした要素とそうでない要素との境界面がはっきりする点が特徴です。こちらも例を見てみましょう。

Gaussian Blur の一例(自サイトより)

先ほどの例とは異なり、ヘッダと背景部分との境界が明確に表れていることが見て取れるかと思います。

Progressive Blur は実際には Gaussian Blur をベースに実装していくのですが、段階的な変化の有無でここまで印象が変わるのは驚きですね。

擬似的 Progressive Blur の実装

構造

本節では、特に Web 上で Progressive Blur を実現する際の構造について述べていきます。

早速構造の説明に入っていきたいところですが、ここで突然「擬似的」というワードが入ってきたことにお気づきになられた方もいらっしゃるかもしれません。先ほど、Progressive Blur とは「段階的にぼかし半径が変わっていくぼかし表現」と述べましたが、実際には Web 上でこれを直接的に表現する手段はありません。したがって、別の代替的手段を用いて擬似的に Progressive Blur を表現していくアプローチを取ります。これを念頭にここからの説明をお読みいただければと思います。

Web 上で Progressive Blur を擬似的に表現するには、Gaussian Blur透明度のある背景色、そしてグラデーションのあるマスクの 3 つを利用します。次の図は Progressive Blur の構造を示した模式図です。

Progressive Blur の構造

上図に示した二つの表現を重ね合わせる(合成)することによって Progressive Blur は実現されます。イメージ的には、単調な Gaussian Blur をグラデーションのあるマスクによって間引いていくことによって、擬似的にぼかしに穴を開けていくような形です。

実装

さて、ここまで Progressive Blur の概要から構造までを述べてきました。本節では先に示した構造をもとに、Web 上での Progressive Blur の実装方法について解説していきます。

前節では、擬似的な Progressive Blur は Gaussian Blur、透明度のある背景色、およびグラデーションのあるマスクの 3 要素によって構成されることを述べましたが、これらは CSS のプロパティにおおよそ一対一で対応します。

  • Gaussian Blur
    • backdrop-filter: blur(16px);
  • 透明度のある背景色
    • background: #ffffff77
  • グラデーションのあるマスク
    • mask-image: linear-gradient(to bottom, transparent, #ffffff 100%);
    • mask-composite: intersect

Progressive Blur は、これらの CSS を全て合わせることによって実装できます。次のコードは Progressive Blur の実装例を示しています。ただし、最小構成を示すためにあえて div などを用いていますが、実際にはセマンティクスに準じた実装をすることを推奨します。

HTML

<div class="card">
    <img class="img" src="./test.jpg">
    <div class="content">
        <p>hoge</p>
    </div>
</div>

CSS

.card {
    width: 320px;
    height: 180px;
    position: relative;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
}

.img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.content {
    width: 100%;
    height: 100%;

    backdrop-filter: blur(16px);
    background-color: #ffffff77;
    mask-image: linear-gradient(to bottom, transparent, #ffffff 100%);
    mask-composite: intersect;
} 

これを表示すると次の画像が示すような形となります。

Progressive Blur の実装例

上記の実装では、.img が適用された画像の上に .content がオーバーレイしています。画像の上に下向きに濃くなっていくマスクを適用したレイヤを重ねることで、擬似的に Progressive Blur を実現できていることがわかります。

懸念

想像にも難くないですが、backdrop-filter: blurmask-* はそれなりに負荷の高い処理になります。それゆえ、多用するとパフォーマンスが劣化する可能性がある点に留意が必要です。

応用

例えば背景色を暗めにしつつ文字を重ねると、透明感のある仕上がりになります。

応用例 1

mask-image が適用されている要素の上に別の要素を置くと、それらにもマスクがかかってしまうため少々薄くなってしまいます。上記の例はその影響で文字が薄くなってしまっています。これを回避するために、さらに別のレイヤを重ねてあげると良いでしょう。

応用例 2

またエラー用のトーストにも使用できます。

応用例 3

まとめ

Progressive Blur は、簡単にモダンなデザインが実現できる面白い UI 表現技法です。こんなデザインも実装できるのか、と新しい発見をしていただけたのであれば幸いです。

最後に、弊社はまだデザインこそしておりませんが「Beyond the Imagination by Software - ソフトウェアで想像を超えろ」をスローガンに、多様なバックグラウンド・技術・属性を持つエンジニアが活躍しています。もし本記事や他の記事を見て弊社にご興味をお持ちいただければ、まずはご連絡ください!

エンジニアリングにご興味のあるあなたを、我々一同お待ちしております!

参考文献

「Type」ScriptでHelloWorldをしてみた Part2

皆さんはTypeScript、使っていますか?TypeScriptっていいですよね。 JavaScriptに型をつけ、ソースコードの型を解析し、実行前に型的な部分の予期しないエラーを防いでくれます。つまり、JavaScriptを静的型付け言語にしたのがTypeScriptというわけです。

ですが皆さん、ちゃんとTypeScriptしてますか??? 日本人が使う/触ったことのある静的型付け言語の代表格といえばC言語(C++)、Java(C#)あたりじゃないでしょうか。難しかった経験、ありますよね?主に「型」への理解が難しくて、PythonやPHPといった型のない世界に逃げ込んだ方が多いんじゃないでしょうか。(どちらも動的型付け言語ではありますが、デフォルトで実行前に型チェックが入っているわけではないので「型がない」と表記しています。) JavaScriptも同じで、もともと型というものは存在していません。ですがTypeScriptにおいては明確に「型」が存在しています。つまり、土台はJavaScriptですが、言語設計としてはちゃんと「型」を使った開発をすることが求められるわけです。

なので皆さん、「「ちゃんとTypeScriptしてますか???」」

まあ変な前置きはここまでにしておいて、 この記事はPart1の「「Type」ScriptでHelloWorldをしてみた」の続編です!

labs.spookies.co.jp

まだPart1見てないよ~って方はPart1を見てからじゃないと内容を理解できないのでそちらをご覧ください~

次にするのは、typeで四則演算を行うtypeを作りたいと思います。 typeで四則演算ってできるの!?って思うかもしれないですが、厳密には自然数だけで割り算以外であればとっても簡単に作れちゃうんです。(割り算は少数になる可能性があり考慮することが多い)

2. 四則演算と数値を作る

2-1. Tupleを錬成する!

type GetRange<N, Acc> =
  Acc extends number[]
    ? Acc[Length] extends N
      ? Acc
      : GetRange<N, [...Acc, Acc[Length]]>
    : never;

Tuple型は再帰を使うと簡単に要素数を増やすことができます! 再帰系型の中でも、Rangeは一番有名じゃないでしょうか。

1. <3, []> => [0]
2. <3, [0]> => [0, 1]
3. <3, [0,1]> => [0, 1, 2]
4. <3, [0,1,2]> => return

指定された数値リテラルのLengthとなるまで「その時配列に入っている要素数」を配列内に入れるという動作を繰り返すと、Rangeした結果のTupleが返却されます。

言ってしまえばこのRangeを取得したいがためにLengthを定義したまでのことはあります。これがないと難しいのじゃ

2-2. 加算と減算を作る!

type Add<Left, Right> = [
  ...GetRange<Left, []>,
  ...GetRange<Right, []>
][Length];

type Sub<Left, Right> =
  GetRange<Left, []> extends [
    ...GetRange<Right, []>,
    ...infer R
  ]
    ? R[Length]
    : never;

Rangeと同じ要領で加算と減算を作成します。

加算はLength + Length、つまり左の数分のTupleと右の数分のTupleをスプレッド構文で展開すると、最終的なlengthは左のTupleのlengthと右のTupleのlengthを足し合わせた状態になります。

3 + 4 => [1,2,3] + [1,2,3,4]
[...[1,2,3], ...[1,2,3,4]] => [1,2,3,1,2,3,4] => 7

減算は、逆に、左のTupleから右のTuple分を消すと最終的な要素数が左の要素数から右の要素数を引いた数になります。 感覚的には、掛け算の約分に近い感じで、同じ要素を取り除くようなイメージです。

5 - 3 => [1,2,3,4,5] - [1,2,3]
[1,2,3,4,5] extends [[1,2,3], ...infer T] => [4,5] => 2

2-3. 0と1を錬成する!

今回の縛りでは、数値リテラルは直接定義できません。じゃあどうするか? Tupleからとってくればいいんです。

type Zero = [][Length]; // 0
type One = [true][Length]; // 1

簡単ですね! ただ、これを何回もやってると長いので、先ほどの計算が生きてくるわけです。 その中でも0と1は数値リテラルの種的な扱いをするのでちょっと特殊なだけです。

2-4. 乗算を作る!

ひたすらに加算しまくるのもめんどくさいので乗算も作っちゃいましょう!

type Mul<Left, Right, Total> =
  Right extends Zero
    ? Total
    : Mul<Left, Sub<Right, One>, Add<Total, Left>>;

これは右側の回数分再帰をして、0になるまでTotalに左の数を加算し続けているだけです。

2 * 3 => 2 + 2 + 2 => 6
8 * 8 => 8 + 8 + 8 + 8 + 8 + 8 + 8 + 8 => 64

あまりやりすぎると、再帰エラーになってしまうので気を付けましょうね...!

2-5. 2と4を作る!

最後に作った四則演算を使って2と3を作ってみましょ。

type Two = Add<One, One>; // 1 + 1 = 2
type Four = Mul<Two, Two, Zero>; // 2 * 2 = 4

わりと直感的に数値を操作できるようになりましたね!いい話 まあ4に関しては別に使う必要ないんですけどね...

3. Hello Worldを錬成する

さあいよいよHelloWorldを錬成する時が来ました!

3-1. 文字リテラルから特定インデックスの文字を取得する関数を作る!

今までの知識を使って文字リテラルのchatAtを作成しましょう!

type CharAt<Str, Index> =
  Index extends Zero
    ? Str extends `${infer FirstChar}${string}`
      ? FirstChar
      : never
    : Str extends `${string}${infer Remain}`
      ? CharAt<Remain, Sub<Index, One>>
      : never;

割と単純ですが、Indexと文字リテラルの頭を引いて行って、Indexが0になったときの頭文字を取得するという感じです。

1. <"abcde", 3> => <"bcde", 2>
2. <"bcde", 2> => <"cde", 1>
3. <"cde", 1> => <"de", 0>
4. <"de", 0> => "d"

今まで出てきた知識を使っているだけなので、勝手がわかれば簡単ですね~

3-2. 中間ポイントを計算しておく

文字テーブルを使うにあたって中間地点からアクセスできるようにちょっとだけ計算しておきます。

type Calc32 = Mul<Mul<Two, Four, Zero>, Four, Zero>;
type Calc20 = Add<Mul<Four, Four, Zero>, Four>;

3-3. 文字を1つづつ取得する!

文字テーブルからcharAtでHelloWorldを取得していきます! ここはもう今まで出てきたのを組み合わせてくだけなので余裕です。

type CharTable = "abcdefghijklmnopqrstuvwxyz!? ";
type h = CharAt<CharTable, Sub<Mul<Four, Two, Zero>, One>>;
type e = CharAt<CharTable, Four>;
type l = CharAt<CharTable, Sub<Mul<Four, Four, Zero>, Add<One, Four>>>;
type o = CharAt<CharTable, Sub<Mul<Four, Four, Zero>, Two>>;
type sp = CharAt<CharTable, Sub<Calc32, Four>>;
type w = CharAt<CharTable, Add<Calc20, Two>>;
type r = CharAt<CharTable, Add<Sub<Calc20, Four>, One>>;
type d = CharAt<CharTable, Add<Two, One>>;
type ex = CharAt<CharTable, Sub<Calc32, Add<Four, Two>>>;

3-4. 組み合わせる!

最後に、先ほど取得した文字を結合していくだけ!

type Result = TupleJoin<[
  Capitalize<TupleJoin<[h, e, l, l, o]>>,
  sp,
  Capitalize<TupleJoin<[w, o, r, l, d]>>,
  ex
]>;

これで、「"Hello World!"」の文字リテラルが推論できました!!!!!!わーい

終わりに

今回はTypeScriptのtype縛りでHello Worldをしてみました! TypeScriptでもここまでのtypeを構築することはそうそうないので、かなりTypeScriptでの型推論という部分に関して深掘りして行けたかなぁ~と思っています。 これをすぐに理解できる方はきっとTypeScriptのプロなんでしょう。 逆にこの記事を理解できれば少なくとも上級者レベルかも???

次は何の言語で遊ぼうかなぁ~

「Type」ScriptでHelloWorldをしてみた Part1

やあこんにちは。🌱だよ。皆さんはこのポストをご覧になったでしょうか?

この投稿の画像、頭で解けた人はいますでしょうか? 実はこれ、最終的には「Hello_Type_World」という文字が推論されるんです! すごいでしょ

今回は上のコードが主に題材となっています!

TypeScriptには俗に言われている「型パズル」というのが存在しています。 が、今回は特別に...!? 「型パズル」ではなく「型プログラミング」をしてみたいと思います。つまり型だけでプログラミングをするんです。処理は書いちゃいけません!

やはり型プログラミングをするうえで一番最初にするのは誰しもやってきた「Hello World」の出力でしょう。 ですが、今回は「型」が題材なので、「出力」ではなく「推論」をしていきます。 もちろん文字列で単にHellow Worldをするのではなく、TypeScriptの「Type」をフルに使った正真正銘の「Type」ScriptでHello Worldをやっていこうと思います!!!!

結果だけ知りたい方のために最終的にはこのコードはこちらです!

type Result = TupleJoin<[
  Capitalize<TupleJoin<[h, e, l, l, o]>>,
  sp,
  Capitalize<TupleJoin<[w, o, r, l, d]>>,
  ex
]>;

ね、簡単でしょう?

え...難しい?じゃあ、順を追ってちゃんと解説していきます!

実行環境

  • TypeScript v5.9.3
  • Target/Module: ESNext
  • TypeScript Playground 標準設定

ルール

  • typeのみ定義可能(つまり関数/値の定義禁止)
  • 文字テーブル以外のlength > 0の直接的な文字リテラルの禁止
  • テンプレートリテラルにおいてプレースホルダー以外の使用を禁止
  • 直接的な数値リテラルの禁止
  • 型定義時のジェネリクス引数のextends及びデフォルト値の禁止
    • type Type<T extends string> みたいな表現ができない代わりにtype Type<T> = T extends stringにしてねという意味
  • object型({ key: value })の定義禁止
  • 文字テーブルを使用し、Resultが"Hello World!"という文字リテラルで推論されていること

説明

1. "length"を作る

文字リテラルが作れない以上"length"は作れないです。 なんで"length"を作らなきゃいけないのか?というと、これはTuple型系の推論をする際に「length」というのはとても重要になります。

type TupleLEngth = [a, b, c, d]["length"];

この場合、TupleLengthは4の数値リテラルを返してくれます。これはTuple型というTypeScriptにある特殊な型形態だからこそできる推論で、基本的にArray.prototype.lengthは静的に要素数を推論できないためnumberを推論しますが、TypeScriptのTuple型は「要素数が必ず固定になっている配列型」という解釈をするため、必ずlengthが一定になる=lengthが数値リテラルで推論できるわけです。

そしてこの推論をするために"length"が必要となるわけですね。

1-1. 標準APIから文字リテラルを抽出する!

しょっぱなから何言ってる変わらないと思いますが、標準APIは文字リテラルの宝庫です。 つまるところ標準APIのプロパティ名や関数名を抽出できれば文字リテラルを定義することなく手文字リテラルを作れちゃうわけですね。

主にここでキーとなってくるのはこの構文です。

type Key = keyof { aaa: 0, bbb: 1 }; // 'aaa' | 'bbb'

keyofはそのオブジェクトのプロパティ名としてアクセス可能なものを文字リテラルのUnionとして返してくれます。 つまりこれで文字リテラルが手に入るわけですね。すばらしい

ただちょっと待って!これはUnionです。つまり完全にその文字リテラルを保証してくれるわけではありません。 つまりUnionじゃなければいいんです。 じゃあどうすればいいのか?オブジェクトを定義する?でも今回のルールでは新規オブジェクトの定義はできません。 オブジェクトからそのプロパティだけを抽出しちゃいましょう!

type IsAny<T> = true extends false & T ? true : false;
type Equal<L, R> = [R] extends [L] ? [L] extends [R] ? true : false : false;
type PickKey<T, P> = {
  [K in keyof T]: IsAny<T[K]> extends false
    ? Equal<T[K], P> extends true
      ? K
      : never
  : never
}[keyof T];

やってることを端的にいうと、オブジェクト内にあるプロパティの型が指定された型に一致するプロパティの値をプロパティ名として、オブジェクトのキーから値(プロパティ名)だけを取り出すということをしています。

例えば、PickKey<typeof Object, typeof Object.defineProperty>とすると"defineProperty"が返ってきます これはtypeof ObjectObjectConstructorを型として、typeof Object.defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>) => Tなので、この型に合って(Equal)かつAnyではないプロパティを探します。

型のEqualって何ぞやって感じですが、B extends Aは数学の集合的な挙動をして、集合記号的にいうとB⊆Aなので、プログラム的に直すとB <= Aです。つまりその逆であるB >= Aをすると、B <= A && B >= Aで、B == Aと同じような意味合いを持つことができます。 しかし、どちらかがUnionである場合注意が必要で、TypeScriptではextendsを通るときにUnion分散というのが行われます。例えばEqual<A | B, A>とすると推論されるのはbooleanとなります。(A==Aであり、A==Bでもあるため、true | falseとなり、結果booleanと解釈される) それを防ぐには、Tuple型に内包して定義をすると防げます。なので、[R] extends [L] ? [L] extends [R]となっているわけですね。

あと、IsAnyで防いでいるのは単にAnyが特殊だからです。 anyというのはTypeScriptにおいては絶対王者と言わんばかりの強制力を持ちます。

type A = 0 & any; // any
type B = 0 & unknown; // 0

anyunknownと違い、「推論を放棄する」というのに近い意味を持つもので、放棄された推論は基本的に何してもanyとなるという挙動をします。 なのでextendsを通しても基本的には必ずtrueになるような挙動を示すため、それを逆手にとってtrue extends false & Tと置くと、any以外は基本的にfalseになります。なのでいい感じにAnyかどうかを判別できるわけです。

1-2. プリミティブから文字リテラルを抽出する!

特殊なプリミティブ型はテンプレートリテラルに通すと文字リテラルを取得できます。 主にtrue, false, null, undefinedの4つは文字リテラルとして推論してくれます。

type True = `${true}`; // "true"
type False = `${false}`; // "false"
type Null = `${null}`; // "null"
type Undefined = `${undefined}`; // "undefined"

1-3. 最初の文字と最後の文字を抜き出す!

さて、ここまで来たら文字を加工する工程になります。 いろんなところから文字リテラルは推論できましたが、lengthを作るためにはちゃんと"length"という文字リテラルを生み出さなくてはなりません。

なので、推論した文字リテラルの一部を拝借できるようにしましょう!

type GetFirstChar<S> = S extends `${infer First}${string}`
  ? First
  : never;
type GetLastChar<S> = S extends `${infer First}${infer Remain}`
  ? Remain extends ''
    ? First
    : GetLastChar<Remain>
  : never;

実はテンプレートリテラルはextendsとinferを使うと面白いことができて、特定の文字リテラルの一部をinferで使用できちゃうんですね~

なので、${infer First}${string}とテンプレートリテラルに入れると、Firstは最初の1文字、その他は何かしらの文字列(string)というextendsができて、Firstを返してあげると最初の1文字だけ返ってくるわけです。

type A = GetFirstChar<"First">; // "F"

最後の文字を取得したいときは、型を再帰的に推論してあげるとできるようになります。 「最初の文字を取り出す」という動作で残った文字列リテラルが、もし空文字列であるならば「それ以上文字がない」ということになるので、取り出した文字を返します。まだ文字が存在するなら、残った文字リテラルで同じ処理を繰り返すといった動作をすることで、結果的に最後の文字が取り出せるというわけです。

// 関数でいうとこういう感じになる
function GetLastChar(S: string) {
  const First = S[0];
  const Remain = S.slice(1);
  if (First !== undefined && Remain !== undefined) {
    if (Remain === "") {
      return First;
    } else {
       return GetLastChar(Remain);
    }
  } else {
    throw;
  }
}

なぜこんな推論ができるかというと、テンプレートリテラルには特殊な推論ルールがあって、以下のルールで推論されるからです。

各型式(infer Tstring)を推論元となる文字列内の部分文字列と左から右へ照合することで進行される。型式の直後にリテラル文字列が続く場合、推論元文字列内でそのリテラル文字列が最初に現れる位置まで、推論元文字列から0個以上の文字を推論することで照合される。 ただし、「型式の直後に別の型式が続く場合、推論元文字列から単一の文字を推論することで照合される。」

引用元: https://stackoverflow.com/questions/76112923/how-do-i-correctly-use-multiple-instances-of-the-infer-keyword-with-template-lit

つまり、

type ContainsUnderFirst<S> =
  S extends `${infer First}_${string}` ? First : never;
type A = ContainsUnderFirst<"_">; // A = ""
type A = ContainsUnderFirst<"aaa_">; // A = "aaa"
type A = ContainsUnderFirst<"_aaa">; // A = ""

みたいな感じになるということです。

1-4. 結合関数を作って1つの文字リテラルを生み出す!

文字リテラルの文字を抽出で来たら結合をして1つの文字リテラルとして生み出しましょう! つまりjoinを作るというわけですね。

type TupleJoin<Tuple> =
  Tuple extends [infer Str extends string]
    ? Str
    : Tuple extends [
        infer Char extends string,
        ...infer Remain
    ]
      ? `${Char}${TupleJoin<Remain>}`
      : never;

これも再帰的にTupleを取り出していって1文字づつ文字リテラルにして言っているだけです。

1. ["a", "b", "c"] // 3. a + bc
2. ["b", "c"] // 2. b + c
3. ["c"] // 1. c

これもTupleとinferで推論をしていて、Tupleでは、通常の配列同様にスプレッド構文が使用できちゃいます。 なので、[infer T, ...infer U]みたいにすると、Tには最初の要素が、Uにはそれ以外の要素が入るような推論をしてくれます。 なのでこのやり方で再帰的に行っていくと最初の要素がどんどんなくなっていくような挙動になるので、要素がなくなったときに文字列を左から埋めていくと、まるでjoinのような挙動を再現できちゃうんですね。

1-5 最後にlengthを錬成する!

さあここまで来たらあとは作った型を元に作るだけ!

type Length =
  TupleJoin<[
    GetLastChar<ToString<null>>,
    GetLastChar<ToString<true>>,
    GetFirstChar<ToString<null>>,
    GetFirstChar<PickKey<typeof globalThis, typeof globalThis>>,
    GetFirstChar<ToString<true>>,
    GetLastChar<PickKey<typeof globalThis, Math>>,
  ]>;

nullの「l」、trueの「e」、nullの「n」、globalThisの「g」、trueの「t」、Mathの「h」 これらを掛け合わせると...念願の"length"が完成!!!!!!

なげぇ!あとめっちゃ無理やり!!!

part2へ...

結構長かったですね。 でも大丈夫です!ここまでのことが理解できていればあとはもう同じような要素を組み合わせていくだけなので、実質ウィニングランみたいな感じです。

次は「「Type」ScriptでHelloWorldをしてみた Part2」でお会いしましょう。

Claudeで設計、Codexが実装:MCPでつなぐAI開発環境

こんにちは!まっちゃんです! 👋

皆さん、Claude Codeは活用できていますか?

私は、現状でもSerena MCPを導入するなどして、結構活用できているように感じています!

ですが、AIを活用する際に、もう一歩前に進む方法を @spookies_yuushi_suzuki と考えてみたので、共有したいと思います!

Claude Code を使って Codex を使ってみる

要するに、どっちもの良いとこ取りをしたいわけですね👀

モデルの得意不得意に決定的な違いがある

現状、Claude Code の使用しているClaude Sonnet 4.5 と、Codexの使用している GPT-5 Codex には、決定的な違いがあるのです

何かというとコンテキストウィンドウの使い方です

どのような長所・短所があるかを図にしてみました👀

モデル名 開発元 コンテキストウィンドウ(入力) 長所(得意な点) 短所(課題・注意点)
Claude Sonnet 4.5 Anthropic 約200K トークン(20万トークン) ・「コンテキストウィンドウ」という制約を超え、タスク全体を通しての流れを把握できる(らしい)
・大量のコードや仕様書を一度に読ませても、理解が破綻しにくい。
・レスポンスは速いが、生成コードがやや大雑把な場合がある。
・数回のプロンプト修正を経ないと、期待通りの実装に至らないことが多い。
GPT-5 Codex OpenAI 約272K トークン(約28万トークン)
参考
・既存コードを正確に読み取り、文脈を踏まえたうえで高精度に実装可能。
・プロジェクト全体の整合性を保ちつつ、動作するコードを高確率で出力できる。
・セッションが長くなると古い履歴を圧縮し、文脈の一部が欠落することがある。
・長期の調査・開発セッションでは、途中経過や思考履歴が失われやすい。
・レスポンスまでが長い。

*1

コンテキストウィンドウが小さいのに、どうして Claude Sonnet 4.5 は大きなコンテキストを扱えるの?というのはここには書ききれないので、 https://www.anthropic.com/news/context-management を参照することとします。

簡単に書くと、不要になったコンテキストを賢く削除したり、重要な情報はメモリに保存することによっていい感じにしているらしいです。

どう生かすの?

Claudeが全体的な設計を、Codexが実装を担当するように分けることで、Claudeの理解力とCodexのコーディング頭脳を両方活かせる(はず)です!

要するに、Claudeは設計者、Codexは実装者として使い分けるのが理想だよね。という感じです 👀

環境構築の前提

まず、CodexとClaude Codeが入っているかつログインされている必要があります。

まだ入っていない方は、入れましょう。

注意

Codex は ChatGPT の Plus 以上のプランが必要です。

https://openai.com/ja-JP/codex/

Claude Code は Pro でも使えなくはないですが、リミットがずいぶん厳しいと思います。

Proで使ってみて、よかったらMax x5 にあげるのがおすすめです

https://docs.claude.com/ja/docs/claude-code/quickstart

実際にやってみた

実際に、Claude Code でCodexを使用できるようにしてみましょう 👀

そのためには、Claude Code にMCP経由でCodexを使えるようにしてあげる必要があります。

下記のコマンドをするといい感じになります。

claude mcp add codex-mcp codex mcp-server

コマンドの構文を解説します。

claude mcp add [名前] [コマンド] [引数]

のような感じです。

なので上のコマンドは、

codex というコマンドに mcp-server という引数をつけたもの( codex mcp-server というコマンド )を、 codex-mcp という名前のMCPとして登録する。のようになりますね。

尚、古い記事だと codex mcp というコマンドで追加するような記事がありますが、もう既に古い構文となっているようです。*2 *3 *4

この状態で、あとはClaude CodeでCodexを使って!と言うだけ!

ではありません!

このままでは、Claude Code 自身がどうCodexを生かせばいいのか知らないからですね!

Claude CodeがCodexを気持ちよく動かすためには??

使うためにはプロンプトをいい感じにしてあげる必要があります。

これを理解するためには、Codexのオプションを理解する必要があります。

approval-policy

簡単に言うと、 Codex自身が、Toolを使う際に許可を求めるかどうか なのですが、このオプションには、"never" | "on-request" | "on-failure" | "untrusted"のどれらが入ります。

それぞれ、

  • never (推奨)
    • どのツールも勝手に使っていい(許可を求めなくていい)
  • on-request
    • Codex 自身が判断して、許可を求める
  • on-failure
    • コマンドが失敗した時に、許可を求める(?)
  • untrusted
    • 信用済みでないコマンドは実行前に許可を求める

のような挙動になっているようです。

その上で、Codexのツールの使用をMCP経由でapprovalするという事に、恐らくClaude Codeが対応していないようなので、on-requestにすると、詰みます。

これであなたもFull Stuck Enginnerです。

sandbox

こちらのオプションは、下のような3つがあります。

  • read-only
    • そのまま。読み取り専用。
  • workspace-write (一番推奨)
    • ワークスペースの中のファイルは編集可能
  • danger-full-access
    • 全部にアクセスできる。破壊し放題

read-onlyをつけられると実作業ができません。

また、Codexくんが動いている間は、進捗が不透明で結構不安になるので、タスクを振る粒度は細かい方が良いでしょう。(多分?)

なので、上記3点を踏まえ、下の方なプロンプトをClaude.mdに突っ込んでおくと良いでしょう。(例なので、いい感じにしてみてください!)

実際の実装に差し掛かる際には、Codex MCPを使用してください。
オプションとして、
workspace には、 workspace-write を
approval-policy には、 never を
つけて、Codex MCPを使用してください。
また、Codexに振るタスクは細分化して、細かく投げて、進捗を追いやすくしてください。

このようにしてあげると、Claude Code は動きやすくなります!

思いついてから突発的に書いているので、感想などはありません!

もっと使ってから、感想だけ追記しておきます!

最後に

いかがでしたか!

これらの考えが参考になれば幸いです!

進化していくAI、追うのも大変ですが、その中で最適解を見つけるために紆余曲折していかなければならないのも、また楽しいですね 👀

また、もしかしたら、Codexの中でCodex MCPを使用することでSubAgents的な動きをできるのでは...?のような考えもあるので、そのようなこともやっていきたいですね 👀

もし、ここが間違ってる!などがあれば是非ご指摘ください 🙏

最後まで読んでいただきありがとうございました!

参照 & 参考

Codexのオプション周り、参考にしました! ありがとうございます 🙏

-> https://zenn.dev/hokuto_tech/articles/97fa88f7805a23

以下ドキュメントなど