前回に続き、 TypeScript Handbook を読んでいく。
- Basic Types
- Variable Declarations (今ココ)
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Variable Declarations
Variable Declarations
let
と const
は JavaScript でも比較的新しい変数宣言です。
let
は var
とよく似ていますが、JavaScript を使う中でよくある "gothas" を避けることができます。
const
は変数への再代入を禁止する、let
の拡張版です。
var
declarations
従来の JavaScript での変数宣言には var
キーワードが使われてきました。
var a = 10;
見て分かる通り、a
という変数を宣言し、値を 10
に設定しています。
メソッドの中で変数宣言することもできますし、
function f() {
var message = "Hello, world!";
return message;
}
他のメソッドから変数にアクセスすることもできます。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // 11
上記の例では f
のなかで宣言された変数 a
を g
がキャプチャしています。
どこで g
が呼び出されても、a
の値は f
の中で宣言されている a
の値に拘束されます。
一度 f()
の実行が終了すれば、g()
が呼ばれていても a
を読み取り/変更することができます。
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // 2
いまいち何の例か分かってないけど、一度
b
に代入してるから、その後にa
を変更しても影響ないということが言いたいのかな?
Scoping rules
var
のスコープは他の言語と比べて奇妙なところがあります。
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // 10
f(false); // undefined
変数 x
は if
ブロックの中で 宣言されているにも関わらず、ブロックの外からでもアクセスすることができます。
なぜなら、var
で宣言された変数は、変数を宣言した関数、モジュール、名前空間、グローバルスコープの中であれば、それがブロックを含むかどうかによらず、どこからでもアクセスすることができるためです。
これは var
スコープ や 関数スコープ とも呼ばれます。
関数の引数も関数スコープとなります。
悪名高き「変数の巻き上げ」というやつだね
このスコープの規則はある種の間違いを引き起こしがちです。
間違いの元になる問題のひとつは、同じ変数を複数回宣言してもエラーにならないことです。
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
i
は同じ 関数スコープの 変数を参照することになるため、内側の for
ループで誤って i
を更新してしまっています。
Variable capturing quirks
以下のコードを実行すると何が起きるでしょう?
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
その結果は以下のようになります。
10
10
10
10
10
10
10
10
10
10
JavaScript 開発者であればよく知っている挙動かもしれませんが、大抵の人は以下のような出力を想像します。
0
1
2
3
4
5
6
7
8
9
前に変数のキャプチャについて説明しましたが、setTimeout
に渡した関数はすべて同じスコープ内の同じ i
を参照しています。
つまり、setTimeout
は数ミリ秒に実行されますが、それは for
ループを抜けた 後 になります。
その時点では i
は 10
になっているため、各関数が 10
を出力するというわけです。
これの回避策としては、各繰り返しで i
をキャプチャするために即時関数 (IIFE) を使うことです。
for (var i = 0; i < 10; i++) {
// 今の 'i' の値をメソッドに引き渡すことで
// 今の状態をキャプチャさせる
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
一見、奇妙に見えますが、この実装パターンは非常によく知られています。
引数の i
は実際には for
ループで宣言されている i
を隠蔽してしまいますが、元の変数名と合わせているため、元のループブロックをそれほど修正せずに済みます。
let
declarations
let
文は宣言用のキーワードを除き、var
と同じように使用できます。
let hello = "Hello!";
var hello = "Hello!";
Block-scoping
let
を使用して宣言された変数は レキシカルスコープ (ブロックスコープ) になります。
var
で宣言された変数は関数内までスコープが広がるのに対し、ブロックスコープの変数はそれを含むブロック、または for
ループ内でしかアクセスできません。
function f(input: boolean) {
let a = 100;
if (input) {
// ここからでも 'a' にアクセスすることができる
let b = a + 1;
return b;
}
// エラー。 ここでは 'b' にはアクセスできない
return b;
}
function f(input) {
var a = 100;
if (input) {
// ここからでも 'a' にアクセスすることができる
var b = a + 1;
return b;
}
// エラー。 ここでは 'b' にはアクセスできない
return b;
}
ここでは a
と b
という 2 つの変数を宣言しています。
a
のスコープは f
全体になるのに対し、b
のスコープは if
ブロック内に制限されます。
コンパイル後には
var
に置き換わるため、あくまでもコンパイル時のチェックである点に注意
(まあそれで充分なんだけど…)
catch
句でも同じルールが適用されます。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// エラー。ここでは 'e' にアクセスできない
console.log(e);
ブロックスコープの変数の他の特徴としては、変数宣言の前に読み書きできないことが挙げられます。
変数自体はスコープ内に "存在" するものの、変数宣言までは temporal dead zone に属するものとして扱われます。
a++; // エラー。まだ 'a' は宣言していない。
let a;
また、変数宣言の前でも変数を キャプチャ することはできますが、変数宣言前にメソッドを呼び出すことは禁止されています。
ES2015 をターゲットとしているのであれば実行時にエラーになりますが、今の TypeScript ではコンパイルエラーにはなりません。
function foo() {
// 'a' をキャプチャすることはできる
return a;
}
// 不正な 'foo' の呼び出し ('a' の宣言前)
// 実行時にエラーになる
foo();
let a;
temporal dead zones についての詳細は Mozilla Developer Network を参照してください。
Re-declarations and Shadowing
var
では何回 変数宣言してもエラーにはなりませんでした。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
上記の例では、すべての x
は 同じ x
を参照しています。
これはバグの原因となりかねませんが、let
では変数宣言は 1 回だけしか許可されません。
let x = 10;
let x = 20; // エラー。同じスコープでは 'x' を再宣言できない
ブロックスコープの変数同士である必要はありません。
function f(x) {
let x = 100; // エラー: 引数と重複している
}
function g() {
let x = 100;
var x = 100; // エラー: 'x' の宣言を複数持てない
}
ただし、ブロックスコープの変数を関数スコープの変数と一緒に宣言できないというわけではありません。
ブロックスコープの変数を異なるブロックで宣言するだけです。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // 0
f(true, 0); // 100
ネストしたスコープ内で新しい変数を宣言することを シャドーイング と呼びます。
これはある種のバグを防ぐものの、別のバグの原因にもなるため、諸刃の剣でもあります。
例えば、前述の sumMatrix
関数を let
を使って記述してみましょう。
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
function sumMatrix(matrix) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i_1 = 0; i_1 < currentRow.length; i_1++) {
sum += currentRow[i_1];
}
}
return sum;
}
変数名が重複したら自動的に連番を振って、名前が重複しないようにしてくれるのね
内側のループの i
が外側のループの i
を隠蔽するため、これは正しく動作します。
意図が明確なコードを書くためにも、通常は シャドーイングは避けるべきですが、いくつかのケースでは有用なこともあるため、自身の判断で適切に使用するようにしてください。
Block-scoped variable capturing
var
宣言のところでキャプチャされた変数の振る舞いについて説明しました。
より直感的に説明すると、スコープが実行されるたびに変数の "環境" が作成され、その環境 (とキャプチャされた変数) はスコープの実行が終わった後も存在し続けるということです。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
環境内に city
がキャプチャされているため、if
ブロックを抜けた後でも変数にアクセスすることができます。
前の方に出てきた setTimeout
の例は、for
ループの繰り返しごとに変数の場外をキャプチャするために IIFE を使用する必要がありました。
これはつまり、変数をキャプチャするために新しい変数環境を作成していたということです。
ループ処理において、let
宣言はまったく違う振る舞いを見せます。
ループ全体に対して新しい環境を作成するのではなく、繰り返しごと に新しいスコープを作成するのです。
これはまさに IIFE を使用して実現しようとしていたことであり、let
宣言を使用して setTimeout
の例を書き直してみましょう。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
var _loop_1 = function (i) {
setTimeout(function () { console.log(i); }, 100 * i);
};
for (var i = 0; i < 10; i++) {
_loop_1(i);
}
for
文の外でi
を宣言してしまうと効果がないので要注意
これの実行結果は以下のようになります。
0
1
2
3
4
5
6
7
8
9
const
declarations
他の変数宣言方法として、const
宣言があります。
const numLivesForCat = 9;
var numLivesForCat = 9;
const
は let
と似ていますが、名前の表す通り、一度束縛した値を変更することはできません。
つまり、let
と同じスコープルールに従いますが、変数に再代入することはできないということです。
ただし、参照先の変数が 不変 であるということと混同しないようにしてください。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// エラー
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// すべて OK
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
他の言語と同じように、オブジェクト (や配列) を定数宣言しても、その中身までは定数にならないということ。
なにかしらの対応を取らない限り、const
変数の内部状態は依然として変更可能なままです。
TypeScript ではオブジェクトのメンバを readonly
に設定することができます。
詳細は Interfaces の章 を参照してください。
let
vs. const
似たようなスコープの振る舞いを持つ変数宣言が 2 つあるため、どちらを使用するべきか聞かれることがよくあります。
そのような広範な質問に対する答えは「場合による」です。
最小権限の原則 に従えば、変更することのない変数にはすべて const
を使用するべきです。
その根拠としては、これまで変更する必要のなかった変数について、何も考えずに値を書き換えるのではなく、変数への再代入が本当に必要かきちんと検討するべきだからです。
また、const
を使用することでデータの流れがより追いかけやすくなります。
最終的には、自分自身が最適と判断したものを採用するか、可能であればチームの他の人と相談してください。
Destructuring
TypeScript は ECMAScript 2015 の機能である変数の分割 (destructuring) に対応しています。
完全なリファレンスは Mozilla Developer Network を参照してください。
Array destructuring
分割代入の一番簡単な例は変数の分割代入です。
let input = [1, 2];
let [first, second] = input;
console.log(first); // 1
console.log(second); // 2
var input = [1, 2];
var first = input[0], second = input[1];
console.log(first); // 1
console.log(second); // 2
この例では first
と second
という 2 つの新しい変数を作成しています。
インデックスを使用しても同じことができますが、こちらの方がより便利です。
first = input[0];
second = input[1];
分割代入は宣言済みの変数に対しても適用可能です。
// 変数の入れ替え
[first, second] = [second, first];
// 変数の入れ替え
_a = [second, first], first = _a[0], second = _a[1];
var _a;
一時変数の宣言が後に来るのが気持ち悪い…
メソッドの引数に適用することもできます。
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);
function f(_a) {
var first = _a[0], second = _a[1];
console.log(first);
console.log(second);
}
f([1, 2]);
...
を使って変数の残り部分を受け取ることができます。
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]
var _a = [1, 2, 3, 4], first = _a[0], rest = _a.slice(1);
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]
もちろん、JavaScript と同じように後続の要素を無視することができます。
let [first] = [1, 2, 3, 4];
console.log(first); // 1
var first = [1, 2, 3, 4][0];
console.log(first); // 1
他の要素を無視することもできます。
let [, second, , fourth] = [1, 2, 3, 4];
var _a = [1, 2, 3, 4], second = _a[1], fourth = _a[3];
Object destructuring
オブジェクトを分割代入することもできます。
let o = {
a: "foo",
b: 12,
c: "bar"
}
let { a, b } = o;
var o = {
a: "foo",
b: 12,
c: "bar"
};
var a = o.a, b = o.b;
この例では o.a
、o.b
から新しい変数 a
、b
を作成しています。
ここでは不要な c
を無視しています。
配列の時と同様に、変数宣言なしに代入することも可能です。
({ a, b } = { a: "baz", b: 101 });
(_a = { a: "baz", b: 101 }, a = _a.a, b = _a.b);
var _a;
この時、文を括弧で囲む必要がある点に注意してください。
JavaScript では通常、{
はブロックの始まりとしてパースされるためです。
...
を使って残りの要素を集めた変数を作ることもできます。
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};
var a = o.a, passthrough = __rest(o, ["a"]);
var total = passthrough.b + passthrough.c.length;
コンパイル後のコードががが…
Property renaming
別名で分割代入することもできます。
let { a: newName1, b: newName2 } = o;
var newName1 = o.a, newName2 = o.b;
この構文を見て混乱しているかもしれませんが、a: newName1
は “a
を newName1
とする” という意味です。
左から右に処理するため、自分で書くとしたら以下のようになります。
let newName1 = o.a;
let newName2 = o.b;
紛らわしいことに、ここでのコロンは型を表して いません。
もし型も指定する場合、分割代入全体の後ろに書く必要があります。
let { a, b }: { a: string, b: number } = o;
var a = o.a, b = o.b;
型指定と別名の指定を同時にする場合、以下のようにする。
TypeScriptlet { a: newName1, b: newName2 }: { a: string, b: number } = o;
Default values
プロパティが存在しない場合のデフォルト値は以下のように指定します。
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
function keepWholeObject(wholeObject) {
var a = wholeObject.a, _a = wholeObject.b, b = _a === void 0 ? 1001 : _a;
}
この例では keepWholeObject
は wholeObject
以外にプロパティ a
と (引数に プロパティ b
が存在しなくても) プロパティ b
を保持することになります。
Function declarations
分割代入はメソッドにも適用できます。
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}
function f(_a) {
var a = _a.a, b = _a.b;
// ...
}
引数のデフォルト値を指定することはよくありますが、分割代入と一緒にデフォルト値を指定する場合はトリッキーになります。
まず最初に以下のパターンを覚えてください。
function f({ a="", b=0 } = {}): void {
// ...
}
f();
function f(_a) {
var _b = _a === void 0 ? {} : _a, _c = _b.a, a = _c === void 0 ? "" : _c, _d = _b.b, b = _d === void 0 ? 0 : _d;
// ...
}
f();
続いて、分割代入されたプロパティのうち、任意のプロパティにデフォルト値を与える方法を覚えてください。
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // OK、デフォルト値 b = 0 が使用される
f(); // OK、デフォルト値 { a: "" } が使用され、さらにデフォルト値 b = 0 が使用される
f({}); // エラー、引数を指定する場合には 'a' は必須
function f(_a) {
var _b = _a === void 0 ? { a: "" } : _a, a = _b.a, _c = _b.b, b = _c === void 0 ? 0 : _c;
// ...
}
f({ a: "yes" }); // OK、デフォルト値 b = 0 が使用される
f(); // OK、デフォルト値 { a: "" } が使用され、さらにデフォルト値 b = 0 が使用される
f({}); // エラー、引数を指定する場合には 'a' は必須
分割代入は注意して使用するようにしてください。
これまでの例で見てきた通り、一番単純な分割代入であってもコードが紛らわしいものになります。
深くネストした分割代入は例えリネーム、デフォルト値、型宣言といったものを組み合わせていなくても、非常に 分かりづらいものになります。
分割代入の式はできるだけ小さく、単純になるようにしてください。
また、いつでも自分自身で分割代入するコードを書くことができます。
Spread
スプレッド演算子 (...
) は分割代入とは逆のもので、配列を他の配列内に、オブジェクトを他のオブジェクト内に展開することができます。
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
var first = [1, 2];
var second = [3, 4];
var bothPlus = [0].concat(first, second, [5]);
この例では、bothPlus は [0, 1, 2, 3, 4, 5]
となります。
スプレッドは first
と second
のシャローコピーを作成しますが、スプレッドによって変更されることはありません。
オブジェクトを展開することもできます。
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
var defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
var search = __assign({}, defaults, { food: "rich" });
この例では、search
は { food: "rich", price: "$$", ambiance: "noisy" }
となります。
オブジェクトの展開は配列の展開よりも複雑です。
配列の展開と同じように左から右に処理していきますが、結果はオブジェクトのままになります。
つまり、前に展開したオブジェクトのプロパティが、より後に展開したオブジェクトで上書きされるということです。
先ほどの例を少し変えてみましょう。
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
var __assign = (this && this.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
var defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
var search = __assign({ food: "rich" }, defaults);
こうすると defaults
の food
プロパティが food: "rich"
を上書きしますが、これは期待する動作ではありません。
オブジェクトの展開にはいくつかの制約があります。
1 つ目の制限は オブジェクト自身の列挙可能なプロパティ のみが展開対象となる点です。
つまり、基本的にオブジェクトインスタンスを展開するとメソッドが失われるということです。
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // OK
clone.m(); // エラー!
2 つ目の制約は、TypeScript コンパイラではジェネリックメソッドの型引数を展開できないことです。
この機能は将来の言語規格に含まれると思われます。