JSONはJavaScript Object Notationの略です。JavaScriptでは{}
で囲まれたキー、プロパティのペアがオブジェクトになります。
JSONを代表とするオブジェクトリテラルをTypeScriptではobject
型で受けることができるのですが、かなり問題があります。
const person: object = {surname: 'Fauré',givenName: 'Gabriel'};console.log(person.surname);// Property 'surname' does not exist on type 'object'.
object
型に代入したオブジェクトはプロパティ、メソッドを一切持っていないものとしてみなされます。そのため6行目のようにプロパティ、メソッドに対するアクセスができません。
これでは何も旨味がないので独自に型を定義しましょう。上記の人名を持つオブジェクトであれば次のように定義できます。
const person: {surname: string;givenName: string;} = {surname: 'Fauré',givenName: 'Gabriel'};person.surname;// -> 'Fauré'
型にもそのままオブジェクトを書いてしまいます。このようにすることで変数person
はsurname
とgivenName
というふたつのプロパティを持っていることが表現でき、アクセスができるようになります。
オブジェクトの型の定義が実体と異なるところは、プロパティの後ろにオブジェクトであれば,
を付けますが型定義では;
を付けます。実際のオブジェクトとオブジェクトの型定義は以下です。
// objectconst obj = {surname: 'Fauré',givenName: 'Gabriel'};// typetype typ = {surname: string;givenName: string;};
この機能を使って円満な家族を表現してみましょう。両親がおり、子供が複数存在します。最近は減りましたが二世帯住宅であれば祖父母もいるでしょう。グローバルな現在では夫婦別姓制もあります。
おそらく変数family
型はこのようになるでしょう
const family: {parents: {mother: {surname: string;givenName: string;};father: {surname: string;givenName: string;};};children: {surname: string;givenName: string;}[];grandparents: {mother: {surname: string;givenName: string;};father: {surname: string;givenName: string;};}[];} = {...};
同じものの繰り返しが多く、再利用性がありません。そこで本項で紹介するタイプエイリアスを使用します。
エイリアスと名前のとおり、ある型に対する別名をつけることが目的です。たとえばはじめに紹介した人物名であれば次のようになります。
type Person = {surname: string;givenName: string;};const person: Person = {surname: 'Fauré',givenName: 'Gabriel'};
タイプエイリアスの中でタイプエイリアスを使うこともできます。これを使えば家族は次のようになるでしょう。なおPerson
は上記のものを使うものとします。
type Parents = {mother: Person;father: Person;};type Family = {parents: Parents;children: Person[];grandparents: Parents[];};const family: Family = {...};
プロパティのgrandparents
は最大ふたつじゃないか、という指摘があるかと思います。そのように設定することもできます。その方法が気になる方はタプルのページをご参照ください。
オブジェクトが持つメソッドの定義の方法はふたつあります。これらが指す関数の意味は厳密には異なりますが、基本的に同じです。差異はかなり踏み込んだ内容になりますので本書では触れません。
type A = {merge: (arg1: string, arg2: string) => string;};type B = {merge(arg1: string, arg2: string): string;};
タイプエイリアスはオブジェクトだけではなく、プリミティブ型に対してもつけることが可能です。
type Surname = string;type GivenName = string;type Person = {surname: Surname;givenName: GivenName;};
上記例はこのように置き換えることも可能です。
ただしSurname, GivenName
はあくまでもstring
型の別名にしかすぎません。つまり次のようなことが問題なく起こってしまいます。
const surname: Surname = 'Fauré';const givenName: GivenName = 'Gabriel';const person: Person = {surname: givenName,givenName: surname};
この例はsurname
とgivenName
を取り違えていますが、これに関してTypeScriptが何かを指摘するということはありません。
たいして役に立たないのではと思われるかもしれませんが、これが大いに役に立つ場面があります。それはユニオン型と組み合わせることです。ユニオン型の説明は専門にありますので詳細は譲ります。ここではユニオン型は|
で区切られたもののどれかがであることだけ覚えておいてください。
type SystemSupportLanguage = 'en' | 'fr' | 'it' | 'es';
これはそのシステムがサポートする言語は'en', 'fr', 'it', 'es'
のいずれかであり、それにSystemSupportLanguage
という別名を与えたことになります。
オブジェクトの型定義をするにあたり便利な記号を紹介します。
これはそのプロパティを選択可(Optional)にする時に使います。つまりあってもなくても構いませんの意味になります。
type Person = {surname: string;middleName?: string;givenName: string;};
上記例にmiddleName
というプロパティを追加し?
を付与しました。こうすればこのタイプエイリアスPerson
はsurname, givenName
は必ず持っているもののmiddleName
は持っていない人がいるということを示しています。
この記号が付与されているプロパティを呼び出す時、使用者はその値があるかないかを確定させる必要があります。ある時はタイプエイリアスとおりの型、つまりこの場合はstring
型ですが、ない時はundefined
として解釈されますのでその判定が必要になります。
これはそのプロパティを読み取り専用(Readonly)にする時に使います。
type Person = {readonly surname: string;givenName: string;};
上記例のsurname
をreadonly
に変更しました。これによりこのプロパティに対しての代入はできなくなります。
const person: Person = {surname: 'Fauré',givenName: 'Gabriel'};person.surname = 'Panda';// Cannot assign to 'surname' because it is a read-only property.person.givenName = 'Gorilla';
もちろんreadonly
がついていないプロパティ、この場合givenName
は代入が可能です。
readonly
はそのオブジェクトが入れ子になっている場合、その中のオブジェクトのプロパティまでをreadonly
にはしません。つまり、再帰的なものではありません。
type Name = {surname: string;givenName: string;};type Person = {readonly name: Name;readonly age: number;};const person: Person = {name: {surname: 'Fauré',givenName: 'Gabriel'},age: 79};person.name = {surname: 'Panda',givenName: 'Gorilla'};// Cannot assign to 'name' because it is a read-only property.person.age = 80;// Cannot assign to 'age' because it is a read-only property.
これらが代入不可能なのはわかるかと思いますが、問題は以下です。
person.name.surname = 'Panda';person.name.givenName = 'Gorilla';
Name
のプロパティsurname, givenName
はともにreadonly
ではないためこのように上書きができてしまいます。
これら記号と、それを発展させた型の表現についてはユーティリティ型に詳細がありますのでご参照ください。
オブジェクトのキーをあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのインデックス型です。
プロパティがすべてstring
型であるようなオブジェクトのタイプエイリアスは以下です。
type Butterfly = {[key: string] : string;};
キーを表している変数key
は別名でも構いません。
もちろんこのButterfly
にはプロパティがstring
型であればなんでも入ります。
const bufferflies: Butterfly = {en: 'Butterfly',fr: 'Papillon',it: 'Farfalla',es: 'Mariposa',de: 'Schmetterling'};
この型の変数を利用する時、どのキーも存在するものとして扱われます。存在しないキーを指定してもエラーが発生することなくundefined
を返します。またTypeScriptによる入力補完も働きません。
bufferflies.ja;// -> undefined
このチェックをより厳密にするオプションがtsconfig.jsonにあります。このオプションを有効にするとたとえプロパティがあるキーにアクセスしてもプロパティの型はundefined
とのユニオン型であると解釈されるようになります。こちらについてはtsconfig.json Deep Diveのページをご覧ください。
インデックス型はstring
型、number
型しか指定できません。
type Jekyll = {[key: boolean]: string;};// An index signature parameter type must be either 'string' or 'number'.
ちなみにnumber
型のキーを持つオブジェクトとは配列のようなオブジェクトのことです。
インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefined
かどうかの型チェックが必要です。入力の形式が決まっているのであればMapped typeの使用を検討できます。
Mapped typeは主にユニオン型と組み合わせて使います。先ほどシステムがサポートする言語を定義しました。
type SystemSupportLanguage = 'en' | 'fr' | 'it' | 'es';
これをインデックス型と同じようにキーの制約として使用することができます。
type Butterfly = {[key in SystemSupportLanguage]: string;};
このようにButterfly
を定義するとシステムがサポートしない言語、ここではde
が設定、使用できなくなります。
const bufferflies: Butterfly = {en: 'Butterfly',fr: 'Papillon',it: 'Farfalla',es: 'Mariposa',de: 'Schmetterling'};// Object literal may only specify known properties, and 'de' does not exist in type 'Butterfly'.
プロパティを読み取り専用にするreadonly
をそのオブジェクトのすべてのプロパティに適用するReadonly<T>
というユーティリティ型があります。他にもユーティリティ型はありますが、それらについては専門のページがありますのでここでは割愛します。
Readonly<T>
もこの機能で実現されています。Readonly<T>
は次のように実装されています。
type Readonly<T> = {readonly [P in keyof T]: T[P];};
keyof T
という見慣れない表現が登場しましたが、これはオブジェクトのキーをユニオン型に変更するものだと解釈してください。つまり次のようなものです。
type Name = {surname: string;middleName: string;givenName: string;};type NameKeys = keyof Name;// -> 'surname' | 'middleName' | 'givenName'
Mapped typeはインデックス型と異なりsymbol
型もキーにすることができます。
type Identifier = symbol | 1;type Sample = {[P in Identifier]: string;};const sample: Sample = {[Symbol('thick')]: 'thin',1: 'pork'};
考え方はユニオン型と相対するものです。ユニオン型がどれかを意味するならインターセクション型はどれもです。言い換えるとオブジェクトの定義を合成させることを指します。
インターセクション型を作るためには合成したいオブジェクト同士を&
で列挙します。
type TwoDimensionalPoint = {x: number;y: number;};type Z = {z: number;};type ThreeDimensionalPoint = TwoDimensionPointal & Z;const p: ThreeDimensionPointal = {x: 0,y: 1,z: 2};
xy平面上の点を表すTwoDimensionalPoint
を拡張してxyz平面上の点のThreeDimensionalPoint
に変換しました。
プリミティブ型のインターセクション型をつくることもできますが、作るとnever
という型ができます。
type Never = string & number;const n: Never = '2';// Type '"2"' is not assignable to type 'never'.
このnever
型にはいかなる値も代入できません。使い道がまるでないように見えますが意外なところで役に立ちます。今回は説明を省きます。
システムの巨大化に伴い、受け付けたいパラメーターが巨大化したとします。
type Parameter = {id: string;index?: number;active: boolean;balance: number;photo?: string;age?: number;surname: string;givenName: string;company?: string;email: string;phoneNumber?: string;address?: string;// ...};
一見してどのプロパティが必須で、どのプロパティが選択可かが非常にわかりづらいです。これをインターセクション型とユーティリティ型のRequired<T>
とPartial<T>
を使いわかりやすく表記できます。ユーティリティ型については解説しているページがありますのでご覧ください。
type Mandatory = {id: string;active: boolean;balance: number;surname: string;givenName: string;email: string;};type Optional = {index: number;photo: string;age: number;company: string;phoneNumber: string;address: string;};
Mantatory
はRequired<T>
を、Optional
はPartial<T>
をつけます。
type Mandatory = Required<{id: string;active: boolean;balance: number;surname: string;givenName: string;email: string;}>;type Optional = Partial<{index: number;photo: string;age: number;company: string;phoneNumber: string;address: string;}>;
これで最初に定義したParameter
と同じタイプエイリアスができました。
type Parameter = Readonly<Mandatory & Optional>;