クラスのインスタンスとしてのオブジェクト以外に、JSONを代表とする{}
で囲まれたオブジェクトリテラルという書き方もかなり一般的です。
オブジェクトリテラル、と聞くとパッと思い浮かばないかもしれませんが、クラスという概念が追加される前のオブジェクトと言えばコレ。のオブジェクトです。
const pokemon = {name: 'pikachu',no: 25,genre: 'mouse pokémon',height: 0.4,weight: 6.0,};
プリミティブ型以外を総称するためにTypeScriptではobject
という型が定義されています。これはどのような形のオブジェクトリテラルも、クラスのインスタンスも、関数も受けることができます。
const pikachu: object = {name: 'pikachu',no: 25,genre: 'mouse pokémon',height: 0.4,weight: 6.0,};const pokemon: object = new Pokemon('pikachu',25,'mouse pokémon',0.4,6.0);const increment: object = i => i + 1;
しかしながら悲しいことにobject
型を与えた変数はその変数の持っているプロパティ、メソッドに対してアクセスができません。
pikachu.no;// Property 'no' does not exist on type 'object'.pokemon.genre;// Property 'genre' does not exist on type 'object'.increment(4);// This expression is not callable.// Type '{}' has no call signatures.
そこでオブジェクトの型を独自に定義することができます。独自に定義した型にエイリアス(別名)をつけて使い回すこともできます。この機能については本書のタイプエイリアスの頁を参照ください。
オブジェクトのプロパティはたとえオブジェクトを定数にしたとしても書き換えができてしまいます。
const pikachu = {name: 'pikachu',no: 25,genre: 'mouse pokémon',height: 0.4,weight: 6.0,};pikachu.name = 'raichu';pikachu;// ->// {// name: 'raichu',// no: 25,// genre: 'mouse pokémon',// height: 0.4,// weight: 6// }
これはJavaScript, TypeScriptが抱えている問題というわけではなく、オブジェクトをリファレンス型として持つ言語では当然の挙動です。
オブジェクトリテラルが定義されたとき、特に型を指定しないとTypeScriptはプロパティの型を推測します。たとえば上記の定数pikachu
はTypeScriptはこのように型を定義します。
type Wild = {name: string;no: number;genre: string;height: number;weight: number;};
そのため、先ほどのようなname
をstring
型で上書きするようなことができてしまいます。ちなみにstring
型ではない型の代入はできません。
pikachu.name = false;// Type 'false' is not assignable to type 'string'.
プロパティを書き換えさせないためには、次のような方法が挙げられます。
readonly
については、タイプエイリアスの頁にて解説がありますので、そちらをご参照ください。
オブジェクトリテラルの末尾にas const
を記述すればプロパティがreadonly
でリテラルタイプで指定した物と同等の扱いになります。
const pikachu = {name: 'pikachu',no: 25,genre: 'mouse pokémon',height: 0.4,weight: 6.0,} as const;
代入はもちろんできません。
pikachu.name = 'raichu';// Cannot assign to 'name' because it is a read-only property.
どちらもオブジェクトのプロパティをreadonly
にする機能は同じですが、以下が異なります。
const assertion
はオブジェクト全体に対する宣言なので、すべてのプロパティが対象になりますが、readonly
は必要なプロパティのみにつけることができます。
オブジェクトの中にオブジェクトがあるときの挙動が異なります。たとえば次のようなオブジェクトがあるとします。
type Country = {name: string;capitalCity: string;};type Continent = {readonly name: string;readonly canada: Country;readonly america: Country;readonly mexico: Country;};const america: Continent = {name: 'North American Continent',canada: {name: 'Republic of Canada',capitalCity: 'Ottawa'},us: {name: 'United States of America',capitalCity: 'Washington, D.C.'},mexico: {name: 'United Mexican States',capitalCity: 'Mexico City'}};
ここでContinent
のタイプエイリアスがもつプロパティはすべてreadonly
です。よって次のようなことはできません。
america.name = 'African Continent';// Cannot assign to 'name' because it is a read-only property.america.canada = {name: 'Republic of Côte d\'Ivoire',capitalCity: 'Yamoussoukro'};// Cannot assign to 'canada' because it is a read-only property.
しかしながら、次のようなことは問題なくできてしまいます。
america.canada.name = 'Republic of Côte d\'Ivoire';america.canada.capitalCity = 'Yamoussoukro';
これはreadonly
をつけたプロパティがオブジェクトである場合に、そのオブジェクトのプロパティまでreadonly
にはしないことに起因します。
as const
を付けます。
const america = {name: 'North American Continent',canada: {name: 'Republic of Canada',capitalCity: 'Ottawa'},us: {name: 'United States of America',capitalCity: 'Washington, D.C.'},mexico: {name: 'United Mexican States',capitalCity: 'Mexico City'}} as const;
readonly
と同様にトップレベルのプロパティへの代入はできません。
america.name = 'African Continent';// Cannot assign to 'name' because it is a read-only property.america.canada = {name: 'Republic of Côte d\'Ivoire',capitalCity: 'Yamoussoukro'};// Cannot assign to 'canada' because it is a read-only property.
これだけではなくオブジェクトが持つプロパティも同様にreadonly
にしてくれます。
america.canada.name = 'Republic of Côte d\'Ivoire';// Cannot assign to 'name' because it is a read-only property.america.canada.capitalCity = 'Yamoussoukro';// Cannot assign to 'capitalCity' because it is a read-only property.
見かたに慣れていないと使いづらい機能ではありますが、分割代入という便利な代入方法があります。
あるタイプエイリアスWild
があったとします(上述のものと同一です)。
type Wild = {name: string;no: number;genre: string;height: number;weight: number;};
このWild
を変数で受けたあとname
とno
とgenre
だけを使いたい時、かつては次のようにする必要がありました。
const pokemon: Wild = safari();const name: string = pokemon.name;const no: number = pokemon.no;const genre: string = pokemon.genre;
これを簡素に代入まで済ませてしまおうというのが分割代入の目的です。
分割代入は、オブジェクトを返す関数などの戻り値に直接オブジェクト自体を書くような方式で使います。たとえば上記の例だとこのようになります。
const {name,no,genre}: Wild = safari();
もちろんheight, weight
が必要なときは書き足せば定数として設定されます。このときは1行目の宣言(今回はconst
)によって変数か定数かが決まるので、変数も定数も欲しい時は分けて書いてください。
オブジェクトの中のオブジェクト、つまりネストした状態でも問題なく使うことができます。先ほど出てきた次の例で考えます。
type Country = {name: string;capitalCity: string;};type Continent = {name: string;canada: Country;us: Country;mexico: Country;};
このような分割代入をすることができます。
const {name,canada: {name},us: {name},mexico: {name}} = america();
しかしながら、この例ではname
という名前が重複してしまっているため、理論上は正しいのですが同じ名前の定数の多重定義となってしまっています。
分割代入はプロパティの名前をそのまま継ながなければならないかというとそうではありません。好きな名前に変更することができます。先ほどのname
が重複してしまった例は次のように書き直せます。
const {name: continentName,canada: {name: canadaName},us: {name: usName},mexico: {name: mexicoName}} = america();
配列にも分割代入を使うことができます。
const [alpha, bravo, charlie, delta, echo] = phone();
配列の中の配列も同様に分割代入を使えます。
const [alpha, [bravo, [charlie, [delta, echo]]]] = phone();
先頭からではなく、特定番目だけ欲しい時は次のように書くこともできます。
const [,,, delta, echo] = phone();
残余引数を使うこともできます。次の例ではalpha
がT
型でrest
はT[]
型になります。
const [alpha, ...rest] = phone();
オブジェクトのキーと変数名が同じ時にかぎり、オブジェクトに値を代入するときも同様に分割代入を使うことができます。次の例がほぼすべてです。
const name: string = 'pikachu';const no: number = 25;const genre: string = 'mouse pokémon';const height: number = 0.4;const weight: number = 6.0;const pikachu: Wild = {name,no,genre,height,weight};
要するにこちらの省略型です。
const pikachu: Wild = {name: name,no: no,genre: genre,height: height,weight: weight};
もちろん一行で書くこともできます。
const pikachu: Wild = { name, no, genre, height, weight };
オブジェクトへの代入も分割代入を理解した上でご覧ください。たとえば先ほどのWild
型の変数を関数に引数として与える時、次のように与えることもできます。
const newPokemon: Wild = evolution({ name, no, genre, height, weight });
また、関数でこれを関数内で分割代入で受け取ることもできます。
function evolution({ name, no, genre, height, weight }: Wild): Wild {// ...}
関数における分割代入については関数のページに詳細がありますので併せてご参照ください。
タイプエイリアスとインターフェースは機能が似通っており、誰もがどちらを使うべきか非常に困惑します。 本書では主にオブジェクトリテラルを指すときはタイプエイリアスを使用していますが、インターフェースを使っても特に問題がありません。 そこで、次にタイプエイリアスとインターフェースの違いを挙げます。
インターフェースはオブジェクトの型を定義することだけができます。プリミティブ型に対してインターフェースを作ることはできません。
type Nil = null;
ユニオン型、インターセクション型はタイプエイリアスのみが受けることができます。このとき、ユニオン型とインターセクション型の対象となるもの(次の場合T, Mandatory, Optional
)はタイプエイリアスでもインターフェースでもどちらでも構いません。
type Nullable<T> = T | null;type Parameter = Mandatory & Optional;
インターフェースはインターセクション型こそできませんが代わりに拡張することができます。
interface Parameter extends Mandatory, Optional {}
Mapped typeはタイプエイリアスのみができます。Mapped typeについてはタイプエイリアスのページを参照してください。
ユーティリティ型のReadonly<T>
はMapped typeで次のように実装されています。
type Readonly<T> = {readonly [P in keyof T]: T[P];};
これをインターフェースで作り直しても動作しません。
interface RO<T> {readonly [P in keyof T]: T[P];};// 'T' is declared but its value is never read.// A computed property name must be of type 'string', 'number', 'symbol', or 'any'.// Member '[P in keyof' implicitly has an 'any' type.// Cannot find name 'P'.// Cannot find name 'keyof'.// Cannot find name 'T'.// Cannot find name 'T'.// Cannot find name 'P'.// ']' expected.// ';' expected.// Declaration or statement expected.// Declaration or statement expected.
インターフェースのみができる機能で、もっともタイプエイリアスと異なる特徴です。
JavaScriptがES2015, ES2016, ES2017, ES2018, ES2019と進化するにつれ、既存のクラスにもメソッドが追加されることもあります。たとえばArray
クラスはES2016でarray.includes()
が、ES2019でarray.flatMap()
が追加されました。
インターフェースではバージョンごとにメソッドのArray
のインターフェースをファイルを分けて定義して、環境に応じて読み込むファイルを変えるだけでArray
の型定義ができます。
// ES2016.array.tsinterface Array<T> {includes(...): boolean;}// ES2019.array.tsinterface Array<T> {flatMap<U>(...): U[];}
もしこれをタイプエイリアスでやるとすれば、次のようになるでしょう。最終的な成果物がArray
となる必要があるため、それまで別の名前で定義して、最後にインターセクション型を使い合成してArray
を作り出す必要があります。
type Array<T> = ES2016Array<T> & ES2019Array<T>;
このDeclaration mergingの機能はポリフィル
を行うライブラリの型定義でよく見ることができます。
これらは大変よく似ています。どれもオブジェクトの型の定義にどれも使うことができます。
const a: object = {};const b: Object = {};const c: {} = {};
また、相互に入れ替えが可能です。
const d: object = a;const e: object = b;const f: object = c;const g: Object = a;const h: Object = b;const i: Object = c;const j: {} = a;const k: {} = b;const l: {} = c;
object
はプリミティブ型ではないのすべてのリファレンス型を総称するものとして定義されています。
Object
はTypescriptで型の定義がされているインターフェースです。そのため.
を入力すればオブジェクトが持っているメソッドの入力補完ができます。
{}
はプロパティ、メソッドを持たないオブジェクトリテラルの型定義です。こちらもobject
と同様に入力補完はできません。
当然ながらプリミティブ型はオブジェクトではありません。そのため、そもそも代入できないのではと思われるかもしれませんが、次のようになります。
const object1: object = undefined;// Type 'undefined' is not assignable to type 'object'.const object2: object = null;// Type 'null' is not assignable to type 'object'.const object3: object = false;// Type 'false' is not assignable to type 'object'.const object4: object = 0;// Type '0' is not assignable to type 'object'.const object5: object = '';// Type '""' is not assignable to type 'object'.const object6: object = Symbol();// Type 'unique symbol' is not assignable to type 'object'.const object7: object = 10n;// Type '10n' is not assignable to type 'object'.const iObject1: Object = undefined;// Type 'undefined' is not assignable to type 'Object'.const iObject2: Object = null;// Type 'null' is not assignable to type 'Object'.const iObject3: Object = false;const iObject4: Object = 0;const iObject5: Object = '';const iObject6: Object = Symbol();const iObject7: Object = 10n;const literal1: {} = undefined;// Type 'undefined' is not assignable to type '{}'.const literal2: {} = null;// Type 'null' is not assignable to type '{}'.const literal3: {} = false;const literal4: {} = 0;const literal5: {} = '';const literal6: {} = Symbol();const literal7: {} = 10n;
object
と異なり、Object, {}
はboolean, number, string, symbol, bigint
型の変数に代入ができてしまいます。
これはTypesScriptの設計がおかしいわけではなくJavaScriptが元々持っているAutoboxingを意味します。
文字数カウントをしたい時はstr.length
とすれば文字数が得られます。また、数値を文字列にしたければ(テンプレートリテラルを使わなければ)num.toString()
とすれば文字列が得られます。
プリミティブ型はオブジェクトではないのでプロパティやメソッドを持っていないはずです。ですがこのようなことができるのは、内部的にはJavaScriptがプリミティブ型の値をオブジェクトに変換しているからです。この暗黙の型変換をAutoboxingと呼びます。
ちなみにこのときに使われるオブジェクトを通称ラッパークラスと呼び、それらのインターフェースもTypeScriptにBoolean, Number, String, Symbol, BigInt
として定義されています。なおundefined
とnull
のラッパークラスはありません。
const bool: Boolean = false;const num: Number = 0;const str: String = '';const sym: Symbol = Symbol();const big: BigInt = 10n;
当然ながらラッパークラスはObject
を親クラスに持っているため、変数の型としてObject, {}
が定義されてしまうとAutoboxingをしたものと解釈され、代入ができます