🤯

ChatGPTに作ってもらった「クラスからメソッドを除いた型を生成する方法」を解読する(TypeScript)

2023/12/12に公開

ChatGPT(3.5)に「tsでクラスからメソッド以外のフィールドだけを抽出した型を使いたい」と聞くと、以下のコードが返ってきました。

class ExampleClass {
  field1: number;
  field2: string;

  method1() {
    // メソッドの実装
  }

  method2() {
    // メソッドの実装
  }

  static staticMethod() {
    // 静的メソッドの実装
  }
}

// メソッド以外のフィールドだけを抽出する型
type FieldsOnly<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};

// ExampleClassからメソッド以外のフィールドだけを抽出
type ExampleFields = FieldsOnly<ExampleClass>;

// ExampleFieldsの型を出力
const exampleFields: ExampleFields = {
  field1: 42,
  field2: "Hello",
  // method1: 不要(メソッドは除外される)
  // method2: 不要
  // staticMethod: 不要(静的メソッドも除外される)
};

色々と書かれていますが重要なのはここだけです。

type FieldsOnly<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};

このコード、TypeScript Playgroundで試してみるとちゃんと動きます。
ですが、TypeScript初心者の僕には意味不明なコードだったので、少しずつ解読していこうと思います。

全体の構造

インデックス型

この型の大枠はインデックス型となっています。インデックス型とは、オブジェクトのプロパティ名を指定せず、プロパティ名の型だけを指定してオブジェクトの型を定義する方法です。
最もシンプルなインデックス型はこのようになります。

// キーがstring, 値がnumberなオブジェクトの型
type IndexType = {
  [K: string]: number
}

// キー名を任意の文字列にできる
const example: IndexType = {
  one: 1,
  two: 2,
}

また、K型変数と呼ばれるもので、その名の通り型を入れられる変数です。ジェネリクス型を使う際によく見かける<T>も型変数です。
(インデックス型ではstringnumbersymbolしか入れられません)

https://typescriptbook.jp/reference/values-types-variables/object/index-signature
https://typescriptbook.jp/reference/generics/type-variables

K in keyof Tの部分

全体の構造がわかったので、ひとまず順番に見ていきます。

in演算子(Mapped Types

[K: string]: numberのようなインデックス型は、一応は型付けされていますが、このままではあらゆるキーを設定できてしまいます。そこで、in演算子とユニオン型を使って使用できるキーを限定できます。

type FavoriteColors = {
  [K in 'red' | 'green' | 'blue']: boolean
}

const myColors: FavoriteColors = {
  red: true,
  green: false,
  blue: true,
}

// blueが含まれていないので怒られる
const myColors2: FavoriteColors = {
  red: true,
  green: false,
}

// 未知のキー「yellow」があるので怒られる
const myColors3: FavoriteColors = {
  red: true,
  green: false,
  blue: true,
  yellow: true
}

この方法はMapped Typesと呼ばれています。

https://typescriptbook.jp/reference/type-reuse/mapped-types
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

keyof演算子

keyofは、オブジェクトのプロパティ名をユニオン型にして返す演算子です。
たとえば、以下の2つは同じ型です。

type Person = {
  name: string
  age: number
}

// PersonKey1とPersonKey2は同じ型
type PersonKey1 = keyof Person
type PersonKey2 = 'name' | 'age'

https://typescriptbook.jp/reference/type-reuse/keyof-type-operator
https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

inkeyofの組み合わせ

in演算子はユニオン型と組み合わせてインデックス型のキーを限定することができ、typeof演算子はオブジェクトのプロパティ名をユニオン型にして返すことができることがわかりました。そこで、これらを組み合わせてみます。

type ColorCodes = {
  red: string,
  green: string,
  blue: string,
}

type FavoriteColors = {
  [K in keyof ColorCodes]: boolean
}

このように、キーの型が同じオブジェクト型を新たに定義できるようになりました。

T[K] extends (...args: any[]) => any ? never : Kの部分

次に、この部分を見ていきます。ここがこの型の核心です。

型の条件分岐(Conditional Types

Typescriptでは型も条件分岐ができます。以下のような構文となっています。

SomeType extends OtherType ? TrueType : FalseType

全体は通常の三項演算子と同じなのでわかりやすいと思います。

extendsですが、これはSomeTypeOtherTypeに代入できるかということを表しています(代入できる場合はtrue)。

つまり、T[K] extends (...args: any[]) => any ? never : Kは、T[K](...args: any[]) => anyに代入できる場合はnever型、そうでない場合はT[K]型だと言っています。

補足

  • (...args: any[]) => anyは、any型の引数を0個以上持ち、any型を返す関数(すべての型の関数のことだと考えていいと思います)
  • never型は何も代入することができない型
  • T[K]T型のオブジェクトのプロパティの値の型

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

型を読む

これで、この型を読めるようになりました。

type FieldsOnly<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};

まず、全体はインデックス型となっています。
キーの型はK in keyof TTのプロパティ名を列挙し、T[K] extends (...args: any[]) => any ? never : Kで関数型のプロパティはneverへ、それ以外はanyへとasアサーションしています。
値の型はそのままT[K]です。

すべての関数はプロパティ名がnever、つまり何も割り当てられないようになっているので、この型を使うとクラスからメソッドを取り除いた型を取得できます。(クラスだけでなく単なるオブジェクトからメソッドを取り除きたいときにも使えると思います)

おわりに

冒頭でも書きましたが、僕はTypeScript初心者です。間違って理解している箇所があるかもしれないので、その場合は指摘していただけるととても助かります。

参考

https://typescriptbook.jp
https://www.typescriptlang.org/docs/handbook/intro.html

Discussion