hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

なるべく楽して JS のライブラリを作ってみる (part2: 実装)

フロントエンドのテックリードをやってます。みゅーとん(@_mew_ton)です。

前回は作りたいライブラリの設計を全部ChatGPT にやってもらいました。

今回は、その実装を楽にすすめていこうと思います。

また、前回と異なり、今回は手動でブログを書いています。ほとんどが失敗の記録です。

TL; DR

3行でまとめます

  • ChatGPT にコードを書かせる際、 TypeScript の型パズルはまだ難しい
  • Google Copilot に1からコードを書かせるのも難しい
  • 型パズルはまだ人力である必要がある

AIに型パズルはまだ早い・・?

そもそもの要件は、「型ガード関数7種を提供する」「型ガード関数を作るための関数を提供する」の2つでした。

さっそく ChatGPT に 型ガード関数を作るための関数を提供する の実装をお願いすると、以下のようになりました。

以下に、指定されたメソッドを作成しました。

type TypeGuard<T> = (value: unknown) => value is T;
type AssertFn<T> = (value: unknown, errMsgFn?: () => string) => asserts value is T;
type EnsureFn<T> = (value: unknown) => T;
type FallbackFn<T> = (value: unknown, defaultValue: T) => T;

function createGuard<T>(typeGuard: TypeGuard<T>): TypeGuard<T> {
  return typeGuard;
}

function createAssert<T>(typeGuard: TypeGuard<T>): AssertFn<T> {
  return (value: unknown, errMsgFn?: () => string): asserts value is T => {
    if (!typeGuard(value)) {
      throw new Error(errMsgFn ? errMsgFn() : 'Type assertion failed');
    }
  };
}

function createEnsure<T>(assertFn: AssertFn<T>): EnsureFn<T> {
  return (value: unknown): T => {
    assertFn(value);
    return value as T;
  };
}

function createFallback<T>(typeGuard: TypeGuard<T>): FallbackFn<T> {
  return (value: unknown, defaultValue: T): T => {
    return typeGuard(value) ? (value as T) : defaultValue;
  };
}

function not<T>(typeGuard: TypeGuard<T>): TypeGuard<Exclude<unknown, T>> {
  return (value: unknown): value is Exclude<unknown, T> => {
    return !typeGuard(value);
  };
}

function createInvertedAssert<T>(typeGuard: TypeGuard<T>): AssertFn<Exclude<unknown, T>> {
  return createAssert(not(typeGuard));
}

function createInvertedEnsure<T>(assertFn: AssertFn<T>): EnsureFn<Exclude<unknown, T>> {
  return createEnsure(createInvertedAssert(assertFn));
}

function createInvertedFallback<T>(typeGuard: TypeGuard<T>): FallbackFn<Exclude<unknown, T>> {
  return createFallback(not(typeGuard));
}

これらの関数を使用して、 `isString` のようなタイプガード関数を生成し、それらを元に
`assertString``ensureString``fallbackString` などの関数を作成できます。
また、それらの反転したバージョンも作成できます。

注意: `createInvertedEnsure``createInvertedFallback` は、
`createInvertedAssert` 関数を使用していますが、これは `createAssert` の逆バージョンです。
逆バージョンを作成するために、 `not` 関数を使用しています。

違う!

一見すごく、よくできています。

多少ロジックは9割がた書けていました。これは非常に良く出来てると思います。

しかし、型推論については残念ながら半分近く間違っていました。

Google Copilot 作戦

ちょうど同じ頃、弊社では GitHub の Enterprise プランへの契約と Google Copilot の導入が始まったので、同様に試してみました。

Google Copilot では途中までの入力に対して、続きを保管してくれます。

試しに、

type TypeGuard<T> =

まで入力すると・・

type TypeGuard<T> = (x: unknown) => x is T;

と補完されました。よさそうです。

しかし、続けて TypeAssert InvertedTypeAssert 入力してみるとこのように補完されました

type TypeAssert<T> = (x: unknown) => asserts x is T;

type InvertedTypeAssert<T> = (x: unknown) => asserts x is Exclude<unknown, T>;

残念ながらこれも間違いです。

何が違うのか?

ChatGPT も Google Copilot も、タイプガードの否定を正しく作れませんでした。

例えば以下のようなユニオン型の定数があったとして・・

const hoge = 'string' as string | number

isString を条件にすれば、if文の内側で hoge は string であると解釈されますし

if (isString(hoge)) {
  // ココで、 hoge は string
}

否定すれば、 number であると解釈されます

if (!isString(hoge)) {
  // ココで、 hoge は number
}

今回設計した assert, ensure, fallback のようなメソッドでは、isXxxx のような ! による否定ができないので、assertNotString のようなメソッドを提供するように設計しています。

以下のコードのように、assertNotString を使えば、 hoge は string ではない型だと解釈されてほしかったのです。

assertNotString(hoge)

// hoge がここで number になってほしい!!

しかし、 ChatGPT, GoogleCopilot 両者とも、hoge は Exclude<unknown, string> という型 (つまり、 unknown と同等の型) だと推論されるコードになっています

つまるところ、InvertedTypeAssert は以下の型が正解でした

type InvertedTypeAssert<T> = <U>(x: T | U) => asserts x is U;

結論

まとめると以下のとおりでした。

  • ChatGPT にコードを書かせる際、 TypeScript の型パズルはまだ難しい
  • Google Copilot に型パズルを1からを書かせるのも難しい
  • 型パズルはまだ人力である必要がある

結局の所、自力でやらざるを得ませんでした。

型推論がちゃんと効く型ガード7種関数を作るための関数と、その型は以下のファイルです。どちらも 100% 人力での実装となりました。

また、これをもとに isString に対応する関数7種 (assertString assertNotString ensureString ensureNotString fallbackString fallbackNotString ) を1回実装したあと、新たに isNumber に対応するなど、他の関数を作る際は GoogleCopilot はかなり真価を発揮しました。

過去の傾向からの推測には非常に強いのを改めて感じました。

まとめ

100% 楽するのはできませんでした。

とはいえ、実装の最初の一歩を人力で対応して以降はほとんどAIによる力で実装が進んだので、ある程度楽できたと思います。

まだまだ実装を 完全に AI に頼り切ることはできないようです。名前の通り Copilot (副操縦士) ですから、任せきりにするのはよくないですね。

引き続き、AIに頼りつつ、今度はOps 周りに手を入れてみようとおもいます。シリーズはまだ続きます。