タイプエイリアス (Type aliases)

JSONJavaScript 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'
};
console.log(person.surname);

型にもそのままオブジェクトを書いてしまいます。このようにすることで変数personsurnamegivenNameというふたつのプロパティを持っていることが表現でき、アクセスができるようになります。

オブジェクトとその型定義で違うところ

オブジェクトの型の定義が実体と異なるところは、プロパティの後ろにオブジェクトであれば,を付けますが型定義では;を付けます。実際のオブジェクトとオブジェクトの型定義は以下です。

// object
{
surname: 'Fauré',
givenName: 'Gabriel'
};
// type
{
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'
};

タイプエイリアスの中でタイプエイリアスを使うこともできます。これを使えば家族は以下のようになるでしょう。

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
};

この例はsurnamegivenNameを取り違えていますが、これに関してTypeScriptが何かを指摘するということはありません。

たいして役に立たないのではと思われるかもしれませんが、これが大いに役に立つ場面があります。それはユニオン型(Union types)と組み合わせることです。ユニオン型の説明は専門にありますので詳細は譲ります。ここではユニオン型は|で区切られたもののどれかがであることだけ覚えておいてください。

type SystemSupportLanguage = 'en' | 'fr' | 'it' | 'es';

これはそのシステムがサポートする言語は'en', 'fr', 'it', 'es'のいずれかであり、それにSystemSupportLanguageという別名を与えたことになります。

宣言に使える特殊な記号

オブジェクトの型定義をするにあたり便利な記号を紹介します。

?

これはそのプロパティを選択可(Optional)にする時に使います。つまりあってもなくても構いませんの意味になります。

type Person = {
surname: string;
middleName?: string;
givenName: string;
};

上記例にmiddleNameというプロパティを追加し?を付与しました。こうすればこのタイプエイリアスPersonsurname, givenNameは必ず持っているもののmiddleNameは持っていない人がいるということを示しています。

この記号が付与されているプロパティを呼び出す時、使用者はその値があるかないかを確定させる必要があります。ある時はタイプエイリアス通りの型、つまりこの場合はstring型ですが、ない時はundefinedとして解釈されますのでその判定が必要になります。

ちなみにstring型あるいはundefinedであればユニオン型を使うとstring | undefinedと書くことができるのですが、これと?では明確に違う点があります。それはundefinedとのユニオン型で書いた場合、宣言時に省略ができなくなります。つまり宣言時に値を持っていないことをundefinedとして明記する必要があります。

type Person = {
surname: string;
middleName: string | undefined;
givenName: string;
};
const person: Person = {
surname: 'Fauré',
givenName: 'Gabriel'
};
// -> Property 'middleName' is missing in type '{ surname: string; givenName: string; }' but required in type 'Person'.

ちなみにこの省略可能なundefinedはTypeScriptではundefined型ではなく、void型に属しています。

readonly

これはそのプロパティを読み取り専用(Readonly)にする時に使います。

type Person = {
readonly surname: string;
givenName: string;
};

上記例のsurnamereadonlyに変更しました。これによりこのプロパティに対しての代入はできなくなります。

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はそのオブジェクトが入れ子になっている場合、その中のオブジェクトのプロパティまでを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ではないためこのように上書きができてしまいます。

これら記号と、それを発展させた型の表現についてはユーティリティ型に詳細がありますのでご参照ください。

Index signatures

オブジェクトのキーをあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのIndex signaturesです。 プロパティが全てstring型であるようなオブジェクトのタイプエイリアスは以下です。

type Butterfly = {
[key: string]: string;
};

キーを表している変数keyは別名でも構いません。

もちろんこのButterflyにはプロパティがstring型であればなんでも入ります。

const bufferflies: Butterfly = {
en: 'Butterfly',
fr: 'Papillon',
it: 'Farfalla',
es: 'Mariposa',
de: 'Schmetterling'
};

さらに、どのキーも存在するものとして扱われます、存在しないキーを指定してもエラーが発生することなくundefinedを返します。またTypeScriptによる入力補完も働きません。

console.log(bufferflies.ja);
// undefined

ユニオン型と合わせる

先ほどシステムがサポートする言語を定義しました。

type SystemSupportLanguage = 'en' | 'fr' | 'it' | 'es';

これはユニオン型のタイプエイリアスですが、これをIndex signaturesのキーとして使用することができます。

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'.

Index signaturesの制限

Index signaturesstring型、number型しか指定できません。

type Jekyll = {
[key: boolean]: string;
};
// -> An index signature parameter type must be either 'string' or 'number'.

ちなみにnumber型のキーを持つオブジェクトとは配列のことです。様々な型をキーに設定したい場合はMapを使用してください。

インターセクション型 (Intersection types)

考え方はユニオン型と相対するものです。ユニオン型がどれかを意味するならインターセクション型はどれもです。言い換えるとオブジェクトの定義を合成させることを指します。

インターセクション型を作るためには合成したいオブジェクト同士を&で列挙します。

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 = 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;
}>;
type Parameter = Readonly<Mandatory & Optional>;