↑こちらの記事の続きです。
前回のあらすじ
前回の記事は、
- 引数が数値であれば、数値の配列を返すべし
- 引数が文字列であれば、文字列の配列を返すべし
- 引数がオブジェクトであれば、オブジェクトの配列を返すべし
- 引数が配列であれば、配列の配列(二次元配列)を返すべし
上記のような制限を持った関数を、
ジェネリクスを使って表現しよう、という内容でした。
コードも軽く見返してみましょう。
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>
の部分が、
T
はHage
型を継承すべし
ということを表しています。
これで、関数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
型の値からなる配列でなくてはならないよ
つまり、
- 引数には、
firstName
とfamilyName
を持ってるやつ(の配列)を渡さないといけないよ- 戻り値も、
firstName
とfamilyName
を持ってるやつ(の配列)を返さないといけないよ
という制限を表現できます。
関数を呼び出す時は、以下のようなコードになります。
const fullNameUserList = getFullNamePersonList<User>(users);
const fullNameWriterList = getFullNamePersonList<Writer>(writers);
なお、関数呼び出し時に<User>
や<Writer>
を省略したとしても、
コンパイラによる型推論が働くため、正しく実行可能です。
HasFullName
インターフェイスを定義しなくてもいい
HasFullName
インターフェイスを使わない書き方だと、
type Person = User | Writer
上記のように、User
とWriter
を許容する
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
インターフェースが書いてあると、
ワイ「あー、フルネームを持ってる人を選別する関数やから」
ワイ「firstName
とfamilyName
を持ってることが要点なんやね」
上記のようなドキュメント的な働きをしてくれるかもしれません。
ジェネリクスで色々なルールを表現できた
このように、ジェネリクスを使用すると
- 引数と返り値は同じ型であるべし
だけでなく、さらに
- 姓と名を持った何らかの型を受け取るべし
といったことも表現できるようになります。
何らかの型、という自由な感じを持ちつつも、
firstName
とfamilyName
は持っていること!
といった具体的な制限を加えることができます。
まとめ
大きめの案件で、様々なオブジェクトを扱う場面が増えると
本当に訳が分からなくなってくることがあります。
そんな時に、型やジェネリクスがあると、例えば
- 姓と名を持った何らかの型を受け取って、返すべし
とか、
色々と取り締まってくれるので、undefined
エラー等が発生しにくく、
開発が捗ります。
ブラウザ上で実行する前に、エディタ上で
「君、違うことしてるで!」
って注意してもらえる訳ですもんね。
面倒くさいようで、ないと逆に困るものだと思います。
〜おしまい〜