【TypeScript】ジェネリクスについて

ゆーたろー

こんにちは、ゆーたろーです。

HRTechベンチャーのエンジニアです。
TypeScript/Vue.js(Nuxt.js)/React/Next.js/PHP/LaravelでWebアプリケーションの開発を行っています。

・副業でSassスタートアップで開発(TS/React/Next.js)
・神戸で勉強会「つながる勉強会」を運営
・神戸メインのグルメインスタ運営

など色々やっています。1児のパパです。

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型を使う人に委ねられます。

ちなみにTの文字は何でも良いです。ジェネリクスの暗黙的なルールで“大文字かつT(型を当てはめるのでTypeのT)”にするのが一般的らしいです。

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を受け取って、argnameプロパティをコンソールに表示します。

console.logの横にも書いていますが、Property 'name' does not exist on type 'T'というコンパイルエラーになります。

.nameでプロパティにアクセスできるのは「nameプロパティを持つオブジェクト型」ですが、今のままでは文字列も数字も配列も受け付けてしまいます。

このようなコンパイルエラーを出さないために型引数に入れる型を制限するにはextendsを使います。

extendsによる型制約にはinterfacetype(型エイリアス)どちらも使えます。

まずは、interface。

interface ObjInterface {
  name: string
}

function sampleFunc1<T extends ObjInterface>(arg: T) {
  console.log(arg.name); // エラーなし
}

このようにTObjInterfaceというinterfaceを実装する形にすることで解決できます。

PHPを例にしますが、オブジェクト指向ではextendsは継承で使われますし、そもそもinterfaceを使うときはimplementsですが、ジェネリクスでの型制限にはextendsとinterfaceを使うのでちょっと混乱しそうですよね。

typeを使った場合。

type ObjType = {
  name: string;
}

function sampleFunc2<T extends ObjType>(arg: T) {
  console.log(arg.name);
}

keyofを使ってより細かく型の制約をつける(ちょっと応用)

ちょっと応用的な話になり、TypeScript初心者には難しいかもしれませんのでそのような方はここは飛ばしてもらってOKです。

ジェネリクスにextendskeyofを組み合わせることでより細かく受け取る型にルールをつけることができます。

まず、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とかも名前だけ聞いたことある程度だからそれもやらなきゃ…)

参考記事

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

この記事を書いた人

Webエンジニア←KHI(プラント事業の技術職)←大学院(機械工学)

PHP/Laravel/TypeScript/React/Next.js(Vue.js/Nuxt.jsは少し)
バックエンド歴の方が長いです。

神戸で「つながる勉強会」を運営↓
https://tsunagaru-kobe.connpass.com/

コメント

コメントする

目次
閉じる