LoginSignup
227
125

More than 3 years have passed since last update.

ジェネリクスをもう少しだけ使いこなす。

Last updated at Posted at 2020-03-25

4歳娘「パパ、具体的な名前をつけないで?」

↑こちらの記事の続きです。

前回のあらすじ

前回の記事は、

  • 引数が数値であれば、数値の配列を返すべし
  • 引数が文字列であれば、文字列の配列を返すべし
  • 引数がオブジェクトであれば、オブジェクトの配列を返すべし
  • 引数が配列であれば、配列の配列(二次元配列)を返すべし

上記のような制限を持った関数を、
ジェネリクスを使って表現しよう、という内容でした。

コードも軽く見返してみましょう。

function toArray<T>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

できあがった関数は上記のようなものでした。

  • 引数がTだったら、戻り値は[T, T, T]であるべし

つまり、

  • 引数がnumberだったら、戻り値は[number, number, number]であるべし

このような制限を、ジェネリクスで表現していましたね。

いただいたコメント

前回の記事を読んだあるユーザーさんから、
こんなコメントをいただきました。

娘の友達「文字列と数値とオブジェクトと数値の配列を受け取れる関数
ってお願いしたのに、真偽値も浮動小数点数もnullも受け取れるじゃない。
それどころか何でも受け取れるわ。これでは使えない。困ったわ」

私は以下のように回答しました。

ガキが・・・
舐めてると潰すぞ

もちろん、潰すというのは冗談です。
少しも潰しません。

もっと制限したい

先ほどのコードは、

  • 引数と同じ型の値からなる配列を返すべし

という要件は満たしていますが、

  • 受け取ることができる引数は、
    文字列または数値またはオブジェクトまたは数値の配列のみとすべし

という条件は満たしていません。

2つの制限を両方とも型で保証するために、extendsを使ってみましょう。

まず、文字列と数値とオブジェクトと数値の配列を許容する型エイリアスを定義します。

type Hage = string | number | object | number[]

これは、

Hage型は、
文字列または数値またはオブジェクトまたは数値の配列ですよ

という意味です。

そして次は、

Tという何らかの型は、Hage型を継承したものであるべし

ということを表現していきましょう。

function toArray<T extends Hage>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

このように書きます。
<T extends Hage>の部分が、

  • THage型を継承すべし

ということを表しています。

これで、関数toArrayは───

  • 引数と同じ型の値の配列を返すべし

上記の条件に加え、

  • 受け取る引数は、
    文字列・数値・オブジェクト・数値の配列のどれかにすべし

───という制限も持つことができました。

試しに真偽値を渡してみます。

const trueArray = toArray(true);

すると、VSCodeに───

trueの引数を、型Hageのパラメーターに割り当てることはできません。

───こんなエラーメッセージが表示されました。
上手くいっているようです。

ちなみにHageという型エイリアスを定義せずに、<T extends string | number | object | number[]>というジェネリクスを書いても同じ効果が得られます。

もっと細かい制限もできる

例えばこんな関数があるとします。

function getFullNamePersonList(persons: User[]): User[] {
  const fullNamePersonList =
    persons.filter(person => person.firstName && person.familyName);
  return fullNamePersonList;
}

ユーザ情報が入った配列の中から、
姓と名を両方持つユーザだけを抽出する関数です。
姓か名を空文字で登録しているユーザは取り除かれます。

このgetFullNamePersonList関数は───

  • Userの配列を受け取って、Userの配列を返すべし

───といった型付けになっています。

Userは以下のようなinterfaceです。

interface User {
  firstName: string
  familyName: string
  tel: number
}

これは───

  • ユーザは、姓と名と電話番号を持つべし

───といった意味になります。

getFullNamePersonList関数は、引数としてUser[]しか受け取れないため、
間違って他の型の値を渡してしまって、
関数が正しく実行できずにエラーになってしまう、といった事故を防げます。

しかし、ユーザだけでなくライターさんも登場

ここでUser以外に、Writerという
姓と名を持つ概念が登場したとします。

Writerは以下のようなinterfaceです。

interface Writer {
  firstName: string
  familyName: string
  articles: article[]
}
  • ライターは、姓と名と記事(配列)を持っている

上記のようなイメージです。
持っているプロパティがUserと微妙に違います。

  • でも、Writerの配列にもgetFullNamePersonList関数を使いたい
  • 姓と名があるのは同じだからね

という状況だとします。

このような場合に、

interface HasFullName {
  firstName: string
  familyName: string
}

上記のようなHasFullNameというinterfaceを定義して、
ジェネリクスを書く際に使用することができます。

HasFullNameは、
getFullNamePersonList関数に必要なプロパティ、
つまり、フルネームを作るのに必要なプロパティだけを定義したinterfaceですね。

  • 姓と名を持つべし

といったイメージです。

getFullNamePersonList関数の型付けは以下のようにします。

function getFullNamePersonList<T extends HasFullName>(persons: T[]): T[] {
  /* 省略 */
}
  • 引数は、HasFullNameを継承したT型の値からなる配列でなくてはならないよ

つまり、

  • 引数には、firstNamefamilyNameを持ってるやつ(の配列)を渡さないといけないよ
  • 戻り値も、firstNamefamilyNameを持ってるやつ(の配列)を返さないといけないよ

という制限を表現できます。

関数を呼び出す時は、以下のようなコードになります。

const fullNameUserList = getFullNamePersonList<User>(users);
const fullNameWriterList = getFullNamePersonList<Writer>(writers);

なお、関数呼び出し時に<User><Writer>を省略したとしても、
コンパイラによる型推論が働くため、正しく実行可能です。

HasFullNameインターフェイスを定義しなくてもいい

HasFullNameインターフェイスを使わない書き方だと、

type Person = User | Writer

上記のように、UserWriterを許容する
Personという型エイリアスを定義して、以下のように書くこともできます。

function getFullNamePersonList<T extends Person>(persons: T[]): T[] {
  /* 省略 */
}
  • 引数も戻り値もPerson[]型じゃないといけないよ

つまり、

  • 引数には、User[]Writer[]を渡さないといけないよ
  • 引数と同じ型の戻り値を返さないといけないよ

というイメージですね。

HasFullNameを定義したほうが、コードの説明書代わりとして良いかも

型エイリアスPersonを使用しても良いのですが、
getFullNamePersonList関数の型付けに使用するジェネリクスとしては、
HasFullNameを定義して<T extends HasFullName>と書く方が

  • 姓と名が要るんやで!

という感じを表現できていて、ワイのようなザコーダーにとっては
脳死で読めて楽かもしれません。

getFullNamePersonListの引数としては、
人間というより姓と名を持ったやーつが必要ですもんね。

interface HasFullName {
  firstName: string
  familyName: string
}

コード内に上記のHasFullNameインターフェースが書いてあると、

ワイ「あー、フルネームを持ってる人を選別する関数やから」
ワイ「firstNamefamilyNameを持ってることが要点なんやね」

上記のようなドキュメント的な働きをしてくれるかもしれません。

ジェネリクスで色々なルールを表現できた

このように、ジェネリクスを使用すると

  • 引数と返り値は同じ型であるべし

だけでなく、さらに

  • 姓と名を持った何らかの型を受け取るべし

といったことも表現できるようになります。

何らかの型、という自由な感じを持ちつつも、

  • firstNamefamilyNameは持っていること!

といった具体的な制限を加えることができます。

まとめ

大きめの案件で、様々なオブジェクトを扱う場面が増えると
本当に訳が分からなくなってくることがあります。

そんな時に、型やジェネリクスがあると、例えば

  • 姓と名を持った何らかの型を受け取って、返すべし

とか、
色々と取り締まってくれるので、undefinedエラー等が発生しにくく、
開発が捗ります。

ブラウザ上で実行する前に、エディタ上で

「君、違うことしてるで!」

って注意してもらえる訳ですもんね。

面倒くさいようで、ないと逆に困るものだと思います。

〜おしまい〜

227
125
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
227
125