ChatGPTに作ってもらった「クラスからメソッドを除いた型を生成する方法」を解読する(TypeScript)
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>
も型変数です。
(インデックス型ではstring
、number
、symbol
しか入れられません)
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
と呼ばれています。
keyof
演算子
keyof
は、オブジェクトのプロパティ名をユニオン型
にして返す演算子です。
たとえば、以下の2つは同じ型です。
type Person = {
name: string
age: number
}
// PersonKey1とPersonKey2は同じ型
type PersonKey1 = keyof Person
type PersonKey2 = 'name' | 'age'
in
とkeyof
の組み合わせ
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
ですが、これはSomeType
をOtherType
に代入できるかということを表しています(代入できる場合は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
型のオブジェクトのプロパティの値の型
型を読む
これで、この型を読めるようになりました。
type FieldsOnly<T> = {
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K];
};
まず、全体はインデックス型となっています。
キーの型はK in keyof T
でT
のプロパティ名を列挙し、T[K] extends (...args: any[]) => any ? never : K
で関数型のプロパティはnever
へ、それ以外はany
へとas
アサーションしています。
値の型はそのままT[K]
です。
すべての関数はプロパティ名がnever
、つまり何も割り当てられないようになっているので、この型を使うとクラスからメソッドを取り除いた型を取得できます。(クラスだけでなく単なるオブジェクトからメソッドを取り除きたいときにも使えると思います)
おわりに
冒頭でも書きましたが、僕はTypeScript初心者です。間違って理解している箇所があるかもしれないので、その場合は指摘していただけるととても助かります。
参考
Discussion