🌚

ChatGPTに勝つ方法

2023/07/15に公開

はじめに

ChatGPTが弱い所

ChatGPTは何でもできるすごい奴に見えますが、実は弱い所があります。
TypeScriptの型システムの理解度があまり高くないようで、複雑な問題を渡すと誤答を繰り返します。
これを突くことで、ChatGPTに勝つことができます。
しかも、この方法で最強のGPT-4に勝てます。

問題1

例えば、以下のような問題を渡すと、ChatGPTは間違えて答えます。

https://github.com/type-challenges/type-challenges/tree/main/questions/00529-medium-absolute

srting, number または bigint を受け取り、正の数の文字列型を出力する Absolute 型を実装して。

例えば

type Test = -100;
type Result = Absolute<Test>; // expected to be "100"

一連のチャット

https://chat.openai.com/share/90c62743-a22d-413f-a52f-894ac60fad73

チャット全文

User
srting, numberまたはbigintを受け取り、正の数の文字列型を出力するAbsolute型を実装して。

例えば

type Test = -100;
type Result = Absolute<Test>; // expected to be "100"
ChatGPT
TypeScriptの型システムには制約があり、文字列や数値を直接操作することはできません。そのため、文字列や数値を操作するような型の操作は、TypeScriptの型システムを使用して実装することはできません。

しかし、値の型に依存する型の振る舞いを定義することは可能です。次のコードスニペットは、この概念を示しています:

type Absolute<T extends number | string | bigint> =
  T extends string
  ? `0${T}` extends infer R ? (R extends `${infer _}-${infer P}` ? P : R) : never
  : T extends bigint
  ? T extends -infer U ? U : T
  : T extends -infer U ? U : T;

この型 Absolute は、stringnumberbigint のいずれかの型を継承します。そして、この型が string の場合、その文字列の中から"-"を取り除く処理を行います。numberbigint の場合は、その数値が負であれば符号を反転させます。

ただし、これは型レベルの操作であり、実際の値の操作ではないことに注意してください。また、この操作は文字列が数値表現であること、または数値が符号付きであることを前提としています。

最後に、上記の実装は"-"が存在する場合のみ有効で、他の不正な文字列が渡された場合のバリデーションは含まれていません。不正な文字列を適切に処理するためには、更なるチェックが必要となります。
User
このコードではエラーが出ます。
-inferは構文エラーです。
ChatGPT
すみません、私のミスでした。-infer という記述はTypeScriptの正式な文法ではないため、それを用いた推論はエラーを引き起こします。

数値やbigintの絶対値を取る型を定義することは、TypeScriptの型システムでは現状不可能です。また、文字列に対しても完全な実装は非常に難しくなります。

ただし、型が特定の文字列(つまり、負の数値を表す文字列)である場合に絶対値を取ることは可能です。以下にその例を示します:

type Absolute<T extends number | string | bigint> =
    T extends `${infer Sign}-${infer Rest}` ? (Sign extends "" ? T : Rest) : T;

type Test = "-100";
type Result = Absolute<Test>; // expected to be "100"

これにより、文字列で表された負の数値の絶対値を取得できます。ただし、この方法は数値またはbigintの型に対しては動作しないため、この限定的なケース以外では実用的ではありません。

最後に、この実装は"-"が存在する場合のみ有効で、他の不正な文字列が渡された場合のバリデーションは含まれていません。不正な文字列を適切に処理するためには、更なるチェックが必要となります。
User
このコードでは"-100"が返ってきます。
期待する型は"100"です。
ChatGPT
すみません、私のミスでした。型が正しく適用されていませんでした。

以下に修正版を示します。ここでは、infer キーワードを使用して、マイナス記号に続く部分を推論しています。次に、その推論された部分を結果の型として使用します。

type Absolute<T extends number | string | bigint> =
    T extends `-${infer Rest}` ? Rest : T;

type Test = "-100";
type Result = Absolute<Test>; // expected to be "100"

この修正版のコードでは、文字列の型が"-"で始まる場合、その"-"を削除します。つまり、"-100"という文字列の型を受け取ると、"100"という文字列の型を返します。

この修正版のコードは、期待する動作を正しく実行します。ただし、このコードは文字列の型のみを処理でき、数値やbigintの型を直接処理することはできません。
User
このコードでは数値型の-100を渡した際にそのまま数値型の-100が戻ってきます。
最初にも言いましたが、srting, numberまたはbigintを受け取り、正の数の文字列型を出力するようにしてください。
ChatGPT
申し訳ありません、私の説明が不足していました。

残念ながら、TypeScriptの型システムは現状では数値やbigintに対して直接操作を行うことはできません。そのため、数値やbigintを受け取り、それを文字列に変換するといった操作は不可能です。

現在のTypeScriptの型システムの制約上、文字列、数値、bigintから文字列への変換は実行時にしかできないため、それを行うための関数を実装することは可能ですが、それは型レベルの操作ではなく、実行時の操作になります。

以下にその例を示します:

function absoluteToString(input: string | number | bigint): string {
    const numberValue = typeof input === "string" ? parseInt(input) : Number(input);
    return Math.abs(numberValue).toString();
}

const test: number = -100;
const result: string = absoluteToString(test); // "100"

この関数 absoluteToString は、文字列、数値、bigintのいずれかを受け取り、その絶対値を文字列として返します。これは、数値やbigintを直接操作できないTypeScriptの型システムの制約を回避する方法です。

しかしながら、この関数は実行時にしか動作せず、また出力は型レベルではなく値レベルであるため、型に依存した処理を行いたい場合には使えません。そのため、現状では要求に完全に応える解決策は存在しないというのが正確な状況です。
User
型で実装できます。以下がこの問いの答えです。

type Absolute<T extends string | number | bigint> = ${T} extends -${infer U}
? U
: T;
ChatGPT
おっしゃる通りです。大変失礼いたしました。以下が修正したコードになります:

type Absolute<T extends string | number | bigint> = `${T}` extends `-${infer U}`
  ? U
  : `${T}`;

type Test1 = "-100";
type Result1 = Absolute<Test1>; // expected to be "100"

type Test2 = -100;
type Result2 = Absolute<Test2>; // expected to be "100"

この修正版のコードでは、numberbigintstring 型に変換してから操作を行います。そのため、これらの型に対する直接的な操作を行うことが可能になります。

指摘していただき、ありがとうございます。

問題2

他にも、以下のような問題を渡すと、ChatGPTは間違えて答えます。

https://github.com/type-challenges/type-challenges/tree/main/questions/00612-medium-kebabcase

以下のコードに適合する型KebabCasをかいて

type FooBarBaz = KebabCase<"FooBarBaz">;
const foobarbaz: FooBarBaz = "foo-bar-baz";

type DoNothing = KebabCase<"do-nothing">;
const doNothing: DoNothing = "do-nothing";

一連のチャット

https://chat.openai.com/share/8625b21d-17de-40c1-a10f-ccb4109ec211

チャット全文

User
以下のコードに適合する型KebabCasをかいて

type FooBarBaz = KebabCase<"FooBarBaz">;
const foobarbaz: FooBarBaz = "foo-bar-baz";

type DoNothing = KebabCase<"do-nothing">;
const doNothing: DoNothing = "do-nothing";
ChatGPT
"KebabCase" という型を定義することで、与えられたキャメルケース(CamelCase)やパスカルケース(PascalCase)の文字列をケバブケース(kebab-case)に変換します。これは、TypeScriptのテンプレートリテラル型と条件型を使用して達成できます。

以下にその実装を示します:

type ToLowerCase<S extends string> =
  S extends `${infer F}${infer R}`
    ? F extends Uppercase<F>
      ? `${Lowercase<F>}${ToLowerCase<R>}`
      : `${F}${ToLowerCase<R>}`
    : S;

type InsertDashBeforeCapital<S extends string> =
  S extends `${infer F}${infer R}`
    ? F extends Uppercase<F>
      ? `-${F}${InsertDashBeforeCapital<R>}`
      : `${F}${InsertDashBeforeCapital<R>}`
    : S;

type KebabCase<S extends string> = ToLowerCase<InsertDashBeforeCapital<S>>;

type FooBarBaz = KebabCase<"FooBarBaz">;
const foobarbaz: FooBarBaz = "foo-bar-baz";

type DoNothing = KebabCase<"do-nothing">;
const doNothing: DoNothing = "do-nothing";

この例では、KebabCase は与えられた文字列型 S をケバブケースに変換します。そのために、まず InsertDashBeforeCapital を使って大文字の前にダッシュ(-)を挿入します。次に、その結果を ToLowerCase で全て小文字に変換します。

これにより、たとえば KebabCase<"FooBarBaz">"foo-bar-baz" となり、KebabCase<"do-nothing"> は既にケバブケースであるため、そのまま "do-nothing" となります。
User
このコードだと-foo-bar-bazとdo--nothingが返ってきます。
書き直してください
ChatGPT
すみません、私の前のコードでは文字列の先頭に大文字がある場合や、ダッシュが連続する場合に正しく動作しないようです。以下に改善した実装を示します:

type Dash = '-';

type ToLowerCase<S extends string> =
  S extends `${infer F}${infer R}`
    ? F extends Uppercase<F>
      ? `${Lowercase<F>}${ToLowerCase<R>}`
      : `${F}${ToLowerCase<R>}`
    : S;

type Kebab<S extends string> =
  S extends `${infer T}${infer U}${infer V}`
    ? U extends Uppercase<U>
      ? `${T}${Dash}${U}${V}` extends infer W
        ? Kebab<W>
        : never
      : S
    : S;

type KebabCase<S extends string> = ToLowerCase<Kebab<S>>;

type FooBarBaz = KebabCase<"FooBarBaz">;
const foobarbaz: FooBarBaz = "foo-bar-baz";

type DoNothing = KebabCase<"do-nothing">;
const doNothing: DoNothing = "do-nothing";

この例では、KebabCase 型は文字列型 S をケバブケースに変換します。まず、Kebab で大文字の前にダッシュ(-)を挿入し、次に ToLowerCase で結果を全て小文字に変換します。この Kebab 関数は再帰的に呼び出され、すべての大文字の前にダッシュを挿入します。
User
下記のエラーが出力されます

型Wは制約stringを満たしていません。
型 '"foo-bar-baz"' を型 '"foobarbaz"' に割り当てることはできません。
ChatGPT
大変失礼しました。この問題は、TypeScriptの文字列に対する操作の限界によるものであり、現在のTypeScriptのバージョンでは「KebabCase」という型を完全に実装することは難しいです。

TypeScriptの文字列操作は、文字列の長さや特定のパターンに基づいていますが、大文字と小文字を個別に扱うことはできません。そのため、ある文字が大文字であるかどうかを判断し、その文字の前にダッシュを追加するという操作は現状ではサポートされていません。

そのため、"KebabCase"のような操作を行うには、実行時にJavaScriptを使用することが一般的です。以下にその例を示します:

function kebabCase(str: string): string {
  return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
}

const fooBarBaz = kebabCase("FooBarBaz");  // "foo-bar-baz"
const doNothing = kebabCase("do-nothing");  // "do-nothing"

ここで定義した kebabCase 関数は、大文字の前にダッシュを追加し、その後で全ての文字を小文字に変換します。これは実行時に行われますので、TypeScriptの型制約による問題は生じません。

しかし、この実行時の操作は型レベルでは表現できませんので、"KebabCase"のような型を正確に定義することはできません。
User
型レベルでできます。以下が答えです

type KebabCase<T> = T extends ${infer L}${infer R}
? R extends Uncapitalize<R>
? ${Uncapitalize<L>}${KebabCase<R>}
: ${Uncapitalize<L>}-${KebabCase<R>}
: T;
ChatGPT
ありがとうございます。そのような方法で KebabCase を表現することも可能でしたね。誤った情報を提供してしまい、申し訳ありませんでした。

この KebabCase 型は、文字列型 T${infer L}${infer R} という形(つまり2つ以上の文字で構成されている)である場合、R が大文字から始まるかどうかを確認します。もし R が大文字から始まる場合(RUncapitalize<R> と同じでない場合)、L の後にハイフンを追加して R を再帰的に処理します。R が小文字から始まる場合は、L の後に直接 R を再帰的に追加します。文字列 T が1文字である場合、そのまま T を返します。

再度、誤った情報を提供してしまったことをお詫び申し上げます。

まとめ

ChatGPTにも弱い所があり、そこを突くことで勝つことができます。
例に挙げた問題は、type-challengesより引用しています。
まあ、こんな型を書く機会はないと思いますが、ChatGPTに勝って優越感に浸ることはできます。
皆さんも是非、ストレスが溜まったらChatGPTに勝って優越感に浸ってイキリトになってください。
俺はキリトだ!(`・ω・´)キリッ

GitHubで編集を提案

Discussion