投稿日:2020年11月16日
TypeScriptのObject型の仕様について仕様例、注意点を含めて詳しくまとめています。
JavaScriptなどの動的型付き言語のObjectの捉え方は静的型付け言語と少々異なっています。
まずはじめにその捉え方の違いについて簡単に説明しておきましょう。
JavaScriptはもともとあるオブジェクトを見る時、どのようなプロパティを持つのかという点のみを重視するようなコードになるという特徴があります。これは動的型付けオブジェクト指向プログラミング言語に特徴的な型付けです。
例えば以下の様な関数があるとします。
/**
* birdのfly()とsing()を呼ぶだけの関数
*
* @param {*} bird fly()とsing()を持つ
*/
function duckTest(bird){
bird.fly();
bird.sing();
}
このduckTest()という関数は引数としてbirdという変数を受け取ります。
関数を見ればわかる通り、この変数birdが満たすべき条件はfly()とsing()というメソッドを実装していることのみで、何のクラスのインスタンスであるかということには一切触れていません。
そのため以下の様にコーディングをすることができます。
/////////// アヒルクラスの場合 /////////////
class Duck {
fly = ()=> {
console.log("バサバサ");
}
sing = () =>{
console.log("グエっグエっ");
}
}
var duck = new Duck();
duckTest(duck);
//////////// コオロギクラスの場合 ///////////
class Cricket {
fly = () =>{
console.log("ブブブブブ");
}
sing = () =>{
console.log("リンリンリン");
}
eat = () => {
console.log("むしゃむしゃ")
}
}
var cricket = new Cricket();
duckTest(cricket);
DuckとCricketは全く違うクラスなのに、このコードは問題なく通ってしまいます。
この様な振る舞いだけを重視した型付けは以下のように比喩され、
"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)
そこからこの型付けの作法はダックタイピングと呼ばれるようになりました。
TypeScriptにおけるObjectは単純にNonPrimitive型を表します。
Objectの生成はJavaScriptと同じ様に オブジェクトリテラル(ブレース{}でプロパティを指定して生成する方法)で生成できます。
ただし型の指定についてなのですが、
アノテーションでobject型を指定してはいけません(禁止されてはいないが避けるべき)。
通常の型指定の方法のアノテーションでobject型を指定した場合、プロパティの利用の時点である問題が起きてしまいます。
まず以下のコードをみてみてください。
let user: object = {firstname: "quai", lastname:"mars"};
console.log(typeof user);
console.log(user.firstname);
このコード、一見問題なさそうですがトランスパイルしてみると次のようなエラーが表示されます。
$ tsc sample.ts
sample.ts:4:18 - error TS2339: Property 'firstname' does not exist on type 'object'.
4 console.log(user.firstname);
~~~~~~~~~
Found 1 error.
firstnameを持っているはずなのに、firstnameが存在しない??
TypeScriptではobject型の指定をした場合、そのオブジェクトがどの様なプロパティを持っているかを認識できず、アクセスすることができません。
そのため、特別な理由がない限りはobject型を指定しないようにしましょう。
どのようなプロパティを持っているかの定義は通常の型アノテーションと同じように指定できます。
let user: {firstname: string, lastname: string} = {
firstname:"quai",
lastname:"mars"};
console.log(typeof user);
console.log(user.firstname)
object
quai
また型推論を使って同様にプロパティをTypeScriptに推測させることができます。
以下のコードではuserがobjectであること、firstname、lastnameがstring型であることをTypeScriptが自動で推論しています。
let user = {
firstname:"quai",
lastname:"mars"};
console.log(typeof user)
console.log(user.firstname)
console.log(typeof user.firstname)
object
quai
string
Objectクラスを使うことでもオブジェクトの作成が可能です。
作成方法はObject()コンストラクタか、Object.create()メソッドを使用します。
let user1 = Object.create({
firstname:"quai",
lastname:"mars"});
let user2 = Object({
firstname: "taichi",
lastname: "yagami"});
console.log(typeof user1)
console.log(user1.firstname)
console.log(typeof user2)
console.log(user2.firstname);
object
quai
object
taichi
オブジェクトの型指定はプロパティとして何が存在するか(形状)のみに焦点をあてているため、
オブジェクトリテラルの他に、条件を満たすクラスのインスタンスを割り当てることもできます。
class User {
firstname: string
lastname: string
}
let user :{ firstname: string, lastname: string} = new User();
user.firstname = "mars";
user.lastname = "quai";
TypeScriptには存在が不確定なプロパティの定義方法も存在します。
以下のコードはuser.lastnameを存在不確定のプロパティとして定義しています。
let user :{
firstname: string
lastname?: string // 存在が不確定のプロパティ
}
user = {firstname: "mars"} // lastnameなしのオブジェクトリテラル
user = {firstname:"mars", lastname: undefined} // lastnameがundefinedのオブジェクトリテラル
user.lastname = "quai" // lastnameを定義することもできる
lastname?のようにキーに?をつけることで、オブジェクトを割り当てる際にこのプロパティを定義しない、もしくはundefinedでの定義が許可されます。
プロパティを[key: T]: Uのように定義することで、その型のキーのプロパティが不特定多数あるという定義をすることができます。
Tにはstring、またはnumberのみ指定することができます。
このような構文のことをインデックスシグネチャ(Index Signature)といいます。
let user: {
username: string,
[key: number]: boolean
}
user = { username : "marsquai" , 1: true, 2: false, 3: true, 100:false};
インデックスシグネチャの[key: T]: Uという構文はkeyという名前である必要はなく、任意の単語を使用できます。
let ranking : {
category: string,
[orderNum: number]: string
}
ranking = {
category: "youtube",
1: "Hikakin",
2: "Hajimesyacho",
3: "Uketsu",
4: "HebiFrog",
5: "Osamurai-san"
}
一つのプロパティにセットできるインデックスシグネチャのキーの型は一つまで、重複させることはできません。
let book : {
name: string,
[key: string]: string
[key: string]: number
}
sample.ts:4:5 - error TS2374: Duplicate string index signature.
4 [key: string]: number
~~~~~~~~~~~~~~~~~~~~~
Found 1 error.
ここまでオブジェクトの作成で全てletを使ってきました。そのため
『プロパティの変更を禁止するにはconstを使えば良いのでは?』
と考えてしまっているかもしれません。
しかし、オブジェクトのプロパティの変更を禁止したい場合に、プリミティブ型のようにconstで定義する方法は使えません。
constでイミュータブル(変更不可)になるのはあくまで『割り当て』であって、プロパティは自由に変更できてしまうのです。
以下のコードをみてみてください。
const user = {firstname: "mars", lastname: "quai"}
user.firstname = "Mr.mars" // プロパティは問題なく変更できる
user = {firstname: "taichi", lastname: "yagami"} // 定数であるため、'user' に代入することはできません。ts(2588)
constで定義したオブジェクトのプロパティが問題なく変更できてしまっています。
そのかわりに、初期化したuserにオブジェクトリテラルを割り当てる動作が失敗しています。
これがconstの動作なのです。
ではプロパティの変更を禁止する場合にはどうすれば良いでしょうか?
これを解決する方法は大きく2つの方法があります。
readonly修飾子を付けたプロパティは一度初期化した後は変更することができなくなります。
let user : {
readonly firstname: string, // readonly
readonly lastname: string // readonly
} = {
firstname: "mars",
lastname: "quai"
}
user.firstname = "Taichi"
sample.ts:9:6 - error TS2540: Cannot assign to 'firstname' because it is a read-only property.
9 user.firstname = "Taichi"
~~~~~~~~~
Found 1 error.
もう一つの方法はObjectクラスのメソッドを使って定義する方法です。
Object.freeze()を使って定義したオブジェクトのプロパティは削除も変更もすることができません。
let user = Object.freeze({
firstname: "mars",
lastname: "quai"
});
user.firstname = "Taichi";
sample.ts:6:6 - error TS2540: Cannot assign to 'firstname' because it is a read-only property.
6 user.firstname = "Taichi";
~~~~~~~~~
Found 1 error.
明示的型指定、型推論のどちらにおいても、宣言時に指定されていないプロパティを宣言後に入れることはできません。
let user1 :{firstname: string, lastname: string} = {firstname: "mars", lastname:"quai"}
let user2 = {firstname: "mars", lastname:"quai"}
// 変数宣言時に定義していないプロパティ
user1.age = 16
user2.age = 20
sample.ts:5:7 - error TS2339: Property 'age' does not exist on type '{ firstname: string; lastname: string; }'.
5 user1.age = 16
~~~
sample.ts:6:7 - error TS2339: Property 'age' does not exist on type '{ firstname: string; lastname: string; }'.
6 user2.age = 20
~~~
Found 2 errors.
変数宣言だけを先にして、そこで型の指定をした場合少しだけ注意をする必要があります。
例えば、型として指定したもとの同じ形状のオブジェクトリテラルを渡す場合は以下のようなコードになります。
let user: {firstname: string, lastname: string}
user = {
firstname: "mars",
lastname: "quai"
}
これは問題ありませんね。
では次に失敗例をみていきましょう。
当然、以下のようにプロパティが足りなかったり、余分なプロパティがある場合にはエラーになります。
let user: {firstname: string, lastname: string}
user = {
firstname: "mars"
}
user = {
firstname: "mars",
lastname: "quai",
age: 10
}
sample.ts:3:1 - error TS2741: Property 'lastname' is missing in type '{ firstname: string; }' but required in type '{ firstname: string; lastname: string; }'.
3 user = {
~~~~
sample.ts:1:31
1 let user: {firstname: string, lastname: string}
~~~~~~~~
'lastname' is declared here.
sample.ts:9:5 - error TS2322: Type '{ firstname: string; lastname: string; age: number; }' is not assignable to type '{ firstname: string; lastname: string; }'.
Object literal may only specify known properties, and 'age' does not exist in type '{ firstname: string; lastname: string; }'.
9 age: 10
~~~~~~~
Found 2 errors.
注意してほしいのは次のようなコードです。
let user2: {firstname: string, lastname: string}
user2.firstname = "mars"
user2.lastname = "quai"
console.log(user2)
上のコードは変数にオブジェクトを渡す前にプロパティを設定しようとしているため、エラーを発生するはずです。
しかし、このコードのトランスパイルは問題なく終了してしまいます。
実際にエラーが発生するのはトランスパイル後のコードの実行時です。
$ node sample.js
/home/ogihara/ts/obj/sample.js:2
user2.firstname = "mars";
^
TypeError: Cannot set property 'firstname' of undefined
at Object.<anonymous> (/home/ogihara/ts/obj/sample.js:2:17)
at Module._compile (internal/modules/cjs/loader.js:1015:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1035:10)
at Module.load (internal/modules/cjs/loader.js:879:32)
at Function.Module._load (internal/modules/cjs/loader.js:724:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
TypeScriptのチェックが万能ではないことは常に心においておいたほうが良さそうです。