TypeScriptで型を使いこなすために重要な機能であるジェネリクスについて自分なりにまとめて残しておきます。

(↑ボケですからね…)
前回はTypeScriptで型を定義する時に使えるinterfaceとtypeについてまとめました。

ジェネリクスとは
わかりやすい説明があるので引用します。
ジェネリクスは、使われるまで型が決まらないようないろいろな型の値を受け入れられる機能を作るときに使います。ジェネリクスは日本語で総称型と呼ばれることもあります。
https://future-architect.github.io/typescript-guide/generics.html
感覚としては型定義する時に「ここはどんな型でもいいから使う時に自由に決めてくれー!」という感じです。
(わかりにくいですよね。)
どんな型でもいいから、とは書きましたが受け入れることができる型を制限することもできます。
ジェネリクスはライブラリを作るために主に用いられる機能であり、一開発者が既にジェネリクスを使って定義された型を使うことはあっても、ジェネリクスを使った型自体を定義をすることはそこまでないのではないかと思います。
使い方
基本
では試しにジェネリクスを使って型を作ってみます。
type GenericsObj1<T> = {
name: string
age: number
generics: T
}
<T>
の部分がジェネリクスです。(型引数と言う)
T
の部分には任意の型を受け入れるので、GenericsObj
型を指定したオブジェクトのgenerics
プロパティの型はGenericsObj
型を使う人に委ねられます。
GenericsObj
型を使うコードを書いています。
// Tにstringを指定
const user1: GenericsObj1<string> = {
name: '太郎',
age: 20,
generics: '文字列'
}
// Tにnumberを指定
const user2: GenericsObj1<number> = {
name: '太郎',
age: 20,
generics: 1000
}
// TにArray<string>(文字列の配列)を指定
const user3: GenericsObj1<Array<string>> = {
name: '太郎',
age: 20,
generics: ['寿司', 'ラーメン', '焼肉']
}
: GenericsObj<string>
や: GenericsObj<number>
のようにT
のところに当てはめる具体的な型を指定することでgenerics
プロパティのvalueの型を動的に指定します。
複数の型引数を設定する
型引数の数に制限はありません。
type GenericsObj2<T, U> = {
name: string
age: number
generics1: T
generics2: Array<U>
}
複数の型引数を使う場合は<>
の中にカンマ区切りで指定します。
const user1: GenericsObj2<string, string> = {
name: '太郎',
age: 20,
generics1: '文字列',
generics2: ['寿司', 'ラーメン', '焼肉']
}
const user2: GenericsObj2<number, number> = {
name: '太郎',
age: 20,
generics1: 1000,
generics2: [1, 11, 111, 1111, 11111]
}
const user3: GenericsObj2<boolean, boolean> = {
name: '太郎',
age: 20,
generics1: true,
generics2: [false, true, false]
}
: GenericsObj2<string, string>
とすることで
- generics1:文字列
- generics2:文字列の配列
という型になります。
型引数に型の制約をつける
ここまで紹介したジェネリクスの型引数はどんな型も許容します。
type GenericsObj1<T> = {
name: string
age: number
generics: T
}
type GenericsObj2<T, U> = {
name: string
age: number
generics1: T
generics2: Array<U>
}
ジェネリクスは関数にも使うことができるのですが、例えば以下の場合を考えます。
function sampleFunc<T>(arg: T) {
console.log(arg.name); // Property 'name' does not exist on type 'T'
}
上記の関数はジェネリクスで指定したT
型の引数arg
を受け取って、arg
のname
プロパティをコンソールに表示します。
console.log
の横にも書いていますが、Property 'name' does not exist on type 'T'
というコンパイルエラーになります。
.name
でプロパティにアクセスできるのは「nameプロパティを持つオブジェクト型」ですが、今のままでは文字列も数字も配列も受け付けてしまいます。
extends
による型制約にはinterface
、type
(型エイリアス)どちらも使えます。
まずは、interface。
interface ObjInterface {
name: string
}
function sampleFunc1<T extends ObjInterface>(arg: T) {
console.log(arg.name); // エラーなし
}
このようにT
をObjInterface
というinterfaceを実装する形にすることで解決できます。
typeを使った場合。
type ObjType = {
name: string;
}
function sampleFunc2<T extends ObjType>(arg: T) {
console.log(arg.name);
}
keyofを使ってより細かく型の制約をつける(ちょっと応用)
ジェネリクスにextends
とkeyof
を組み合わせることでより細かく受け取る型にルールをつけることができます。
まず、keyofって何か?という方のために簡単にコードで説明します。
type K = keyof { name: string, age: number }
// Kは 'name' | 'age' のUnion型になる
const user: K = 'name' // OK
keyof オブジェクト
という構文にすることでオブジェクトのキーを抽出してUnion型の型を生成します。その際に、文字列としてのキーの型となります。
というわけでジェネリクス、extends
と組み合わせて使ってみます。
function sampleFunc3<T extends { name: string, age: number }, U extends keyof T>(
value: T,
key: U
): void {
console.log(value[key]);
}
sampleFunc3({name: '太郎', age: 30}, 'age'); // 30
パッと見ややこしいですが、U extends keyof T
という部分で「sampleFunc3の第二引数には第一引数T( { name: string, age: number }の制約あり)のキー(文字列)のどれかしか受け付けない」というルールになります。
例えば以下のようにname
でもage
でもない文字列を第二引数に指定すると案の定コンパイルエラーになります。
sampleFunc3({name: '太郎', age: 30}, 'height');
// Argument of type '"height"' is not assignable to parameter of type '"name" | "age"'.
余談
Array
を使った配列型や、ユーティリティ型もジェネリクスが使われています。
// 配列型
const array: Array<number> = [1, 2, 3, 4, 5];
// ユーティリティ型(Partial型のみ記載)
type User = {
name: string;
age: number;
isAdult: boolean;
}
const obj: Partial<User> = {
name: '太郎'
};
ユーティリティ型についてはこちらに書いています。

また、Reactの話になりますが、useState
というReact Hooksもジェネリクスで型定義を行います。
type User = {
name: string
age: number
}
export const Sample = () => {
const [user, setUser] = useState<User>({
name: '',
age: 0
})
// 略
}
React、Next.jsを使っていると、それぞれで準備されている型を使うときに詳細に型定義するためにジェネリクスは結構出てきますので、ジェネリクスを読めるようにするだけでもTypeScriptだけでなくReact、Next.js(Vue.js、Nuxt.jsにも)にも活きます。
最後に
自分で記事を書いていてジェネリクスの使い方とかジェネリクスを使ったコードの読み方への理解が深まりました。
TypeScript公式サイトのPlay Groundで実際にコードを検証しながら記事を書いていくので時間はかかりますが確実に理解できていることを感じます。
(全く勉強していない状態で実務でTypeScriptのコードを読んでいく中で初めて<>
を見た時の焦りを思い出しました…笑)
次は型アサーション(as)の使い方についてまとめてみようと思います。
(Mapped Typeとかも名前だけ聞いたことある程度だからそれもやらなきゃ…)
参考記事

コメント