TypeScript "infer" Tips
この記事は KINTOテクノロジーズアドベントカレンダー2024 の2日目の記事です🎅🎄
はじめに
こんにちは!
新車サブスク開発G、Osaka Tech Lab 所属の high-g(@high_g_engineer)です。
最近は、type-challenges という TypeScript の型パズル問題集を業務前に取り組むことを日課にしています。
本記事では、TypeScript の型システムの中でも少し癖のある infer
の Tips をいくつか紹介します。
まずは、infer の解説の前に、infer を利用する上で必須の Conditional Types
について説明します。
Conditional Types とは
Conditional Types は、条件型、型の条件分岐とも言われ、型レベルで条件分岐を可能にする型システムの機能です。
以下のように記述します。
type ConditionalTest<A> = A extends 'a' ? true : false
上記の右辺は、以下のような意味になります。
- 型 A が リテラル型 'a' に代入可能な場合、true 型となる
- 型 A が上記以外の場合、false 型となる
一般的なプログラミング言語の三項演算子と同じような挙動ですね。
ちなみに、ここでの extends
キーワードは、一般的なオブジェクト指向プログラミングにおける継承とは異なる意味を持ちます。
この場合の extends は型の互換性(assignability)を確認するキーワードになります。
infer とは
inferとは、「推論する」を意味し、Conditional Types 内でのみ利用できるキーワードで、TypeScript 2.8 から導入されました。
以下のように記述します。
type InferTest<T> = T extends (infer U)[] ? U : never
右辺では、Conditional Types を利用し、extends の右側で取得したい型を infer ◯
で記述します。
上記の型の場合、型 T が「任意の要素型の配列」であれば、その要素の型を返すという意味になります。
(※ここでの never は、条件に合致しない場合に返される型です)
(infer U)[]
は、任意の要素型の配列を表す型なので、string[]、number[]、boolean[] などあらゆる配列型が該当します。
なので、型 T が number[] だった場合、型の解決は以下のようになります。
type Result = InferTest<number[]> // number
あくまでここに示したのは一例で、これ以外にも infer の利用方法は多数存在します。
infer を利用した関数型の操作
戻り値の型を取得する場合
const foo = (): string => 'Hello, TS!!'
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type FunctionReturn = MyReturnType<typeof foo> // string
先程と同じ要領で Conditional Types を記述し、extends の右側で、取得したい型を記述します。
今回は、戻り値の型を取得したいので、extends の右側に関数の型を記述し、戻り値の部分に infer を記述して完了です。
これは TypeScript の組み込みユーティリティ型 ReturnType<T>
と同じ挙動になります。
引数の型を取得する場合
const foo = (arg1: string, arg2: number): void => {}
type MyParameters<T> = T extends (...args: infer Arg) => any ? Arg : never
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
引数はタプル型になるため、残余引数(スプレッド構文)を利用することで、引数が任意の数の場合でも対応できるようになります。
Conditional Types + 取得したい型 + infer を記述することで型を抽出できます。
これは TypeScript の組み込みユーティリティ型 Parameters<T>
と同じ挙動になります。
infer を利用した配列型(タプル型)の操作
末尾の要素を取得したい場合
タプル型の先頭要素の型を取得する場合、以下のような型定義で解決します。
type Tuple = [number, '1', 100]
type GetType = Tuple[0] // number
しかし、タプル型の末尾の要素の型を取得したい場合、 Tuple[length-1]
の様な記述は TypeScript では出来ません。
この解決方法として、ベストなのが infer です。以下のような型定義になります。
type ArrayLast<T> = T extends [...infer _, infer Last] ? Last : never
[...infer _, infer Last]
で、型 T が配列型またはタプル型の場合に、末尾の要素の型を Last として抽出します。
type Test1 = ArrayLast<[1, 2, 3]> // 3
type Test2 = ArrayLast<[string, number, boolean]> // boolean
type Test3 = ArrayLast<[]> // never
infer を利用したリテラル型の操作
リテラル型の先頭の文字の型を取得する場合
type LiteralFirst<T extends string> = T extends `${infer First}${string}` ? First : never
${infer First}${string}
は、文字列の先頭の文字を First として抽出し、残りの部分を string として扱います。
リテラル型の先頭を大文字にして取得する場合
type FirstToUpper<T extends string> = T extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : never
上記は先程と同じ様に、先頭とそれ以外に分けて文字列を処理し、Uppercase<First>
でユーティリティ型を使用して、先頭の文字を大文字に変換し、残りの文字列と結合します。文字列が空の場合は never 型を返します。
リテラル型の先頭と末尾から空白文字、改行文字などを取り除く場合
type Space = ' ' | '\n' | '\t'
type Trim<S extends string> = S extends `${Space}${infer T}` | `${infer T}${Space}` ? Trim<T> : S;
Space という空白文字、改行文字を格納した型を作成し、Conditional Types の条件に当てはまる場合、型 Trim を再帰的に適用することで、文字列の先頭と末尾の空白文字を取り除くことができます。
まとめ
これらの例のように、infer を利用することで、ある型から欲しい部分を抜き出せるため、型を表現する際の自由度が格段に上がります。
少し癖があるため、慣れるまでに時間がかかりますが、非常に便利な機能です。
型を利用することで堅牢な開発が実現できますが、型を正しく表現しきれないと以下のようなリスクが生じます。
- 不要な型定義によるコード可読性の低下
- 不必要に複雑な型定義によるメンテナンスコストの増大
- 型安全性の低下
infer をはじめとした TypeScript の型システムを適切に活用することで、簡潔かつ明確な型表現を実現し、開発生産性と品質向上を常に心がけられるようになっていきましょう。
関連記事 | Related Posts
We are hiring!
【フロントエンドエンジニア(コンテンツ開発)】新車サブスク開発G/東京
新車サブスク開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 』のWebサイトの開発、運用をしています。業務内容トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、自社サービスである、クルマのサブスクリプションサービス『KINTO ONE』のWebサイトコンテンツの開発・運用業務を担っていただきます。
【フロントエンドエンジニア】新車サブスク開発G/東京
新車サブスク開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO ONE 』のWebサイトの開発、運用をしています。業務内容トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、自社サービスである、TOYOTAのクルマのサブスクリプションサービス『KINTO ONE』のWebサイトの開発、運用を行っていただきます。