はじめに
TypeScriptのMapped Types、入門記事を始めとする色んな記事を読んでも全然理解できなかったんですが、最近ようやく理解できるようになりました。
それで丁寧に解説した日本語の記事に需要があると思ったので、書いていきたいと思います。
Mapped Typesの利用用途
まずは具体的な使いどころを学ぶことでやる気出していきましょう💪
Mapped Typesの利用用途ですが、「元からある型によく似た型を作る」のが多いですね。特にAPIのリクエストとレスポンス関係が典型的かと。例えば本のデータベースを検索する際の例です。
//すんません、型名の頭にTつけたい派です
//キーに来るのはname,author,priceだが設定されているかどうかは分からない(=Optional)
type TSearchBooksRequest = {
name ?: string,
author ?: string,
price ?: number
}
//レスポンスは全部の情報が載っている(検索系あるある)
type TSearchBooksResponse = {
name: string,
author : string,
price :number
}
普通に書くとこんな感じになります。しかしこれにはいくつか問題があります。
- Optionalであるかどうかの違いだけで、よく似てるのに何度も書くのしんどい
- リクエストの型が変わったら、レスポンスの型も手作業で直す必要がある
この辺りを解決するのがMapped Typesです。
ちなみにMapped Typesとは別にUtility Typesというものもあるのですが、Mapped Typesでよくある変形パターンをまとめたものも含まれているので、まだの人は学習していきましょう。
実はこのパターンはMapped Typesを使わなくてもUtility TypesのRequiredで解決します。
type TSearchBooksResponse = Required<TSearchBooksRequest>
「じゃ、Utility Typesだけでいいじゃん」ってなるかと思いますが、あくまでよくあるパターンを纏めただけなので、Utility Typesじゃ足りない場合はMapped Typesを使う必要があります。
1.簡単な記法から読めるようになろう
Mapped Typesの難しさの一つは、実用しようとするとジェネリクスやkeyofといったその他の文法が絡んでくるので、必要知識量が増えるということにあります。なのでとりあえずジェネリクスやkeyof抜きの簡単な記法を読めるようにすることが先決です。
type TBook = {
[key in "name"|"author"]: string;
}
これは「keyは"name"か"author"、valueは文字列のオブジェクト型」です。普通は
type TBook = {
name:string,
author:string
}
って表現するので、実用度はありませんが、これをすらすら読めるようになるよう解説していきます。
1-1.オブジェクトの「動的なキー名」の記法に慣れよう
地味にこのステップは大事です。なんたってTypeScriptのドキュメントのMapped Typesの項目にも最初に載ってますからね。
さて、TypeScriptどころかJavaScriptの話になりますが、「オブジェクトのキー名に変数を使いたい」場合、どうしますか?
const bookName = "TypeScriptの本"
const author = "田中 一郎"
const bookNameAndAuthorMap = {
// 書名をキーとして、valueをプロパティに設定したい
}
この場合、
let bookDictionary = {} //letに変えざるを得ない……
bookDictionary[bookName] = author
みたいに書く人って多いと思うんですよ。でも実は、
const bookDictionary = {
[bookName] : author
}
という風にキーの部分を[]で囲ってやると中の変数名が代入されて、ちゃんと"TypeScriptの本": "田中 一郎" みたいな形で設定できるんですよ。
TypeScriptにおいてもキー名に変数名を使いたい際に、
type TBookDictionary = {
[key: string]: string
}
という形で「keyは動的で何が入るか分からないが、とにかく文字列」という指定ができます。ただこれだと縛りが弱すぎる=エディタでキー部分の補完も効かないので、使い勝手が良くありませんが……。
ちなみにこの"key"という文字列はTSで指定されたキーワードではなく、別に他の文字列でもかまいません。とりあえずここでは「TypeScriptでもキー名に[]を使うことで変数名を使うことができる」とだけ覚えていってください。
またkeyという変数を宣言だけしてるので気色悪いかもしれませんが、TSの文法上、型の世界のみ「使わないけど変数を宣言せざるをえない」ケースが多いです。我慢してください。
1-2.「オブジェクトのキー部分はUnion型で表現できる」という発想に慣れよう
ここ飛躍があるというか、直感的じゃないので慣れてないと詰まるかと思います。JavaScriptのfor-in構文を使って説明していきます。
const book = {
name : "TypeScriptの本",
author : "田中一郎"
}
for(const key in book){
//省略
}
さて(実際に型は設定できないのですが)"const key"に型をつけるとしたらどうなるでしょうか。
for-in文の対象がbookであることから、keyには"name"もしくは"author"が来ることが確定していますよね?ということはTypeScript流に表現すると「"name" | "author"」型です!イェイ!🎉
1-3. いざ読んでみよう
この段階で簡単な記法は読めるようになります。とはいっても最後に一山あるのですが……。1で載せた構文を再掲します。
type TBook = {
[key in "name"|"author"]: string;
}
今度は何となく読めるのではないでしょうか。キーの型指定部分が全てですね。
[]をつけることで変数であることを表していて、"in 【Union型】"という表記で絞込を行っているイメージです。
正直**「in」使うから分かりづらいのであって、そのまま[Property:【Union型】]じゃいかんのか**という気はするのですが、文字列の解析の効率や難易度とか表現の幅の自由度とか、何か理由があるのでしょう……。
2. 実用的な構文を読めるようになろう
Mapped Typesの説明でよく出てくる構文です。
type TModifiedTypes<Type> = {
[Property in keyof Type]: string;
}
1-3をクリアしているのであれば、もはや詰まるところはMapped Typesというよりかはジェネリクスとkeyofだけです。
特にジェネリクスに関しては抽象度が高いのと、T,S,Uと一文字に略する慣習があるのでとっつき辛いイメージがありますが、Mapped Typesに使う上では、外部から別の型情報をもらうための記法ぐらいのノリでも大丈夫です。関数の引数の仕組みを型情報に使えるようにしたみたいな。ワイもちゃんと分かってませんし😱
一文字の慣習も別に特殊キーワードとして指定されてる訳でもないので、ジェネリクスよく分からんっていう人は最初のうちはTypeとか普通に書いちゃっても大丈夫です。TypeScriptのドキュメントだってそう書いてます。
次にkeyofですが、JavaScriptでもオブジェクトから(JSのゆるゆるな)型情報を引っ張るためのキーワードとして設定されています。TypeScriptにおいても"keyof 【型名】"で型のキー情報をUnion型でぶっこ抜くことができます。keyofだけ単独で先に学んでしまうと、「何でUnion型で返してくるんだ/何に使えというんだ」と困惑するかもしれませんが、1-2で説明した通り、オブジェクトのキーはUnion型として表現されるのでMappedTypesでは多用されます。
というわけで、上記は「Typeで渡された型と、キー名は一緒で、valueは文字列型」のオブジェクト型となります。
3.実践しよう
Mapped Typesを学んでどこで使うの?と思われるかもしれませんが、自分が体験して、説明するのにうってつけだと感じたケースを題材に説明したいと思います。
Laravel(PHPの有名フレームワーク)製APIとの通信部を書いていて、バリデーションエラーのレスポンス(リクエストが間違っている際、Laravelが自動でどう間違っているかの説明文章を生成してレスポンスとして返してくれる)をJSで受け取って、画面に表示したいという場面がありました。
ところがエラーのレスポンスが
{
"name" : ["書名は200文字以下です", "書名に絵文字は使えません"],
"author" : ["作者名に特殊文字は使えません"]
//以下引っかかったキー名が列挙
}
こんな感じになってるんですね。要するにkeyにリクエストのkeyが入り、valueはArray<string>
です。
流石にUtility Typesにもこのパターンに対応するものが無いので、この記事の一番最初に書いた、「本の検索」を例にしてバリデーションエラーにも型付けしてみます。
type TSearchBooksRequest = {
name ?: string,
author ?: string,
price ?: number
}
type TValidationError<Type> = {
[Property in keyof Type]: Array<string>;
}
type TSearchBooksValidationError = TValidationError<TSearchBooksRequest>
// 以下が1行で定義される!しかも<>の中に別のリクエストの型を渡せばいくらでも生成可能!😲
// type TSearchBooksValidationError = {
// name?: Array<string>,
// author?: Array<string>,
// price?: Array<string>
// }
こんな形で定義することができます。これは一番簡単なケースですが、Mapped Types自体にもさらにおまけ機能としてreadonly属性をつけ外しする機能がついてたり、TypeScriptの他の記法を組み合わせることで、もっとヤバい(=表現力の強い≒読みにくい)型を定義することができるので、興味のある方は調べてみてください。