let
とconst
は、JavaScriptにおける変数宣言の比較的新しい概念です。前述したように、let
はいくつかの点でvar
に似ていますが、JavaScriptでユーザーが遭遇する一般的な「落とし穴」を回避できます。
const
は、変数への再代入を防ぐという点でlet
の拡張です。
TypeScriptはJavaScriptの拡張であるため、この言語は自然にlet
とconst
をサポートしています。ここでは、これらの新しい宣言と、それらがvar
よりも好ましい理由について詳しく説明します。
JavaScriptを簡単に使用したことがある場合は、次のセクションで復習すると良いでしょう。JavaScriptのvar
宣言のすべての癖に精通している場合は、先に進む方が簡単かもしれません。
var
宣言
JavaScriptでの変数の宣言は、従来からvar
キーワードで行われてきました。
ts
var a = 10;
お分かりのように、a
という名前の変数を値10
で宣言しました。
関数内で変数を宣言することもできます。
ts
function f() {var message = "Hello, world!";return message;}
そして、他の関数内から同じ変数にアクセスすることもできます。
ts
function f() {var a = 10;return function g() {var b = a + 1;return b;};}var g = f();g(); // returns '11'
上記の例では、g
はf
で宣言された変数a
を取得しました。g
が呼び出された時点では、a
の値はf
内のa
の値に結び付けられます。f
の実行が終了した後でも、g
はa
にアクセスして変更できます。
ts
function f() {var a = 1;a = 2;var b = g();a = 3;return b;function g() {return a;}}f(); // returns '2'
スコープルール
var
宣言には、他の言語に慣れている人にとっては奇妙なスコープルールがあります。次の例を見てください。
ts
function f(shouldInitialize: boolean) {if (shouldInitialize) {var x = 10;}return x;}f(true); // returns '10'f(false); // returns 'undefined'
この例に驚いた読者もいるかもしれません。変数x
はif
ブロック内で宣言されていますが、そのブロックの外からアクセスできました。これは、var
宣言は、後で説明する関数、モジュール、名前空間、グローバルスコープ内のどこにでもアクセスできるためです。これはvar
スコープまたは関数スコープと呼ばれることがあります。パラメーターも関数スコープです。
これらのスコープルールは、いくつかの種類のミスを引き起こす可能性があります。それらが悪化する問題の1つは、同じ変数を複数回宣言してもエラーにならないという事実です。
ts
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;}
経験豊富なJavaScript開発者にとってはすぐに気付く点かもしれませんが、内部のfor
ループは、i
が同じ関数スコープの変数を参照しているため、変数i
を誤って上書きしてしまいます。経験豊富な開発者ならご存じのとおり、このようなバグはコードレビューをすり抜け、無限のフラストレーションの源になる可能性があります。
変数キャプチャの特異性
次のスニペットの出力がどうなるか、少し考えてみてください。
ts
for (var i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 100 * i);}
ご存知ない方のために説明すると、setTimeout
は、(他の処理が停止するのを待つものの)一定のミリ秒後に関数の実行を試みます。
準備はよろしいでしょうか?見てみましょう。
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
ループの実行が停止した後です。for
ループの実行が停止するまでに、i
の値は10
になっています。そのため、指定された関数が呼び出されるたびに、10
が出力されるのです!
一般的な回避策は、IIFE(すぐに実行される関数式)を使用して、各反復でi
をキャプチャすることです。
ts
for (var i = 0; i < 10; i++) {// capture the current state of 'i'// by invoking a function with its current value(function (i) {setTimeout(function () {console.log(i);}, 100 * i);})(i);}
この一見奇妙なパターンは、実際にはかなり一般的です。パラメータリストのi
は、for
ループで宣言されたi
を実際にシャドウしますが、同じ名前を付けたため、ループ本体を大きく変更する必要はありませんでした。
let
宣言
ここまでで、var
にはいくつかの問題があることがお分かりいただけたと思いますが、まさにこれがlet
文が導入された理由です。使用されるキーワードを除けば、let
文はvar
文と同じように記述されます。
ts
let hello = "Hello!";
重要な違いは構文ではなく、セマンティクスにあります。これから詳しく見ていきましょう。
ブロックスコープ
let
を使用して変数を宣言すると、レキシカルスコープまたはブロックスコープと呼ばれるものを使用します。スコープが包含する関数にリークするvar
で宣言された変数とは異なり、ブロックスコープ変数は、最も近い包含ブロックまたはfor
ループの外からは見えません。
ts
function f(input: boolean) {let a = 100;if (input) {// Still okay to reference 'a'let b = a + 1;return b;}// Error: 'b' doesn't exist herereturn b;}
ここでは、ローカル変数a
とb
が2つあります。a
のスコープはf
の本体に限定され、b
のスコープは包含するif
文のブロックに限定されます。
catch
節で宣言された変数にも、同様のスコープルールが適用されます。
ts
try {throw "oh no!";} catch (e) {console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
ブロックスコープ変数のもう1つの特性は、実際に宣言される前に読み書きできないことです。これらの変数はスコープ全体に「存在」しますが、宣言までのすべてのポイントは、その一時的デッドゾーンの一部です。これは、let
文の前にアクセスできないという、洗練された言い方です。幸いなことに、TypeScriptはそれを教えてくれます。
ts
a++; // illegal to use 'a' before it's declared;let a;
宣言前にブロックスコープ変数をキャプチャすることはできます。ただし、宣言前にその関数を呼び出すことはできません。ES2015をターゲットにしている場合、最新のランタイムはエラーをスローします。ただし、現時点ではTypeScriptは許容的で、これをエラーとして報告しません。
ts
function foo() {// okay to capture 'a'return a;}// illegal call 'foo' before 'a' is declared// runtimes should throw an error herefoo();let a;
一時的デッドゾーンの詳細については、Mozilla Developer Networkの関連コンテンツを参照してください。
再宣言とシャドウイング
var
宣言では、変数を何回宣言しても、1つしか得られないことを説明しました。
ts
function f(x) {var x;var x;if (true) {var x;}}
上記の例では、x
のすべての宣言は実際には同じx
を参照しており、これは完全に有効です。これはしばしばバグの原因になります。ありがたいことに、let
宣言はそれほど寛容ではありません。
ts
let x = 10;let x = 20; // error: can't re-declare 'x' in the same scope
TypeScriptが問題であることを教えてくれるために、変数が両方ともブロックスコープである必要はありません。
ts
function f(x) {let x = 100; // error: interferes with parameter declaration}function g() {let x = 100;var x = 100; // error: can't have both declarations of 'x'}
ブロックスコープ変数を関数スコープ変数で宣言できないという意味ではありません。ブロックスコープ変数は、明確に異なるブロック内で宣言する必要があります。
ts
function f(condition, x) {if (condition) {let x = 100;return x;}return x;}f(false, 0); // returns '0'f(true, 0); // returns '100'
よりネストされたスコープに新しい名前を導入することをシャドウイングといいます。これは、偶発的なシャドウイングによって特定のバグを独自に導入する一方、特定のバグを防ぐことができるため、両刃の剣です。例えば、以前のsumMatrix
関数をlet
変数を使用して記述していたとします。
ts
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;}
このループバージョンは、内部ループのi
が外部ループのi
をシャドウするため、実際に合計を正しく実行します。
シャドウイングは、より明確なコードを作成するために、通常は避けるべきです。それを利用するのが適切なシナリオもありますが、最善の判断をしてください。
ブロックスコープ変数のキャプチャ
var
宣言による変数キャプチャのアイデアに触れたとき、キャプチャされた変数の挙動について簡単に説明しました。これについてより直感的に理解するために、スコープが実行されるたびに、変数の「環境」が作成されます。その環境とそのキャプチャされた変数は、そのスコープ内のすべての処理が終了した後も存在できます。
ts
function theCityThatAlwaysSleeps() {let getCity;if (true) {let city = "Seattle";getCity = function () {return city;};}return getCity();}
その環境内からcity
をキャプチャしたため、if
ブロックの実行が終了した後でも、それにアクセスできます。
以前のsetTimeout
の例では、for
ループの各反復で変数の状態をキャプチャするためにIIFEを使用する必要がありました。実際に行っていたのは、キャプチャされた変数に対して新しい変数環境を作成することでした。これは少し面倒でしたが、幸いなことに、TypeScriptではもう二度と行う必要はありません。
let
宣言は、ループの一部として宣言された場合、まったく異なる動作をします。ループ自体に新しい環境を導入するのではなく、これらの宣言は各反復ごとに新しいスコープを作成します。これはIIFEで行っていたことと同じなので、古いsetTimeout
の例をlet
宣言を使用するように変更できます。
ts
for (let i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 100 * i);}
そして、予想通り、これは以下を出力します。
0 1 2 3 4 5 6 7 8 9
const
宣言
const
宣言は、変数を宣言するもう1つの方法です。
ts
const numLivesForCat = 9;
let
宣言に似ていますが、名前が示すように、バインドされると値を変更できません。つまり、let
と同じスコープルールを持ちますが、再代入することはできません。
これは、参照する値が不変であるという意味と混同しないでください。
ts
const numLivesForCat = 9;const kitty = {name: "Aurora",numLives: numLivesForCat,};// Errorkitty = {name: "Danielle",numLives: numLivesForCat,};// all "okay"kitty.name = "Rory";kitty.name = "Kitty";kitty.name = "Cat";kitty.numLives--;
特別な対策を取らない限り、const
変数の内部状態は変更可能です。幸いなことに、TypeScriptではオブジェクトのメンバーをreadonly
として指定できます。「インターフェースに関する章」に詳細があります。
let
とconst
同様のスコープセマンティクスを持つ2種類の宣言があるため、どちらを使用すべきかを尋ねるのは自然です。ほとんどの広範な質問と同様に、答えは状況によります。
最小権限の原則を適用すると、変更する予定のない宣言はすべてconst
を使用する必要があります。その理由は、変数を書き換える必要がない場合、同じコードベースで作業している他の開発者がオブジェクトに書き込むことができず、変数に再代入する必要があるかどうかを検討する必要があるためです。const
を使用すると、データの流れを推論する際のコードの予測可能性も向上します。
最善の判断を行い、該当する場合はチームの他のメンバーと相談してください。
このハンドブックの大部分は、let
宣言を使用しています。
デストラクチャリング
TypeScriptが備えるECMAScript 2015のもう一つの機能として、デストラクチャリングがあります。詳細については、Mozilla Developer Networkの記事を参照してください。このセクションでは、簡単な概要を説明します。
配列デストラクチャリング
デストラクチャリングの最も単純な形式は、配列デストラクチャリング代入です。
ts
let input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
これにより、first
とsecond
という2つの新しい変数が作成されます。これはインデックスを使用することと同等ですが、はるかに便利です。
ts
first = input[0];second = input[1];
デストラクチャリングは、既に宣言されている変数でも機能します。
ts
// swap variables[first, second] = [second, first];
そして、関数の引数でも機能します。
ts
function f([first, second]: [number, number]) {console.log(first);console.log(second);}f([1, 2]);
リスト内の残りのアイテムを変数に格納するには、...
構文を使用します。
ts
let [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
もちろん、これはJavaScriptなので、不要な末尾の要素を無視することもできます。
ts
let [first] = [1, 2, 3, 4];console.log(first); // outputs 1
他の要素も同様です。
ts
let [, second, , fourth] = [1, 2, 3, 4];console.log(second); // outputs 2console.log(fourth); // outputs 4
タプルデストラクチャリング
タプルは配列のようにデストラクチャリングできます。デストラクチャリング変数は、対応するタプルの要素の型を取得します。
ts
let tuple: [number, string, boolean] = [7, "hello", true];let [a, b, c] = tuple; // a: number, b: string, c: boolean
タプルの要素の範囲を超えてデストラクチャリングしようとすると、エラーになります。
ts
let [a, b, c, d] = tuple; // Error, no element at index 3
配列と同様に、...
を使用してタプルの残りの部分をデストラクチャリングし、より短いタプルを取得できます。
ts
let [a, ...bc] = tuple; // bc: [string, boolean]let [a, b, c, ...d] = tuple; // d: [], the empty tuple
または、末尾の要素、またはその他の要素を無視できます。
ts
let [a] = tuple; // a: numberlet [, b] = tuple; // b: string
オブジェクトデストラクチャリング
オブジェクトもデストラクチャリングできます。
ts
let o = {a: "foo",b: 12,c: "bar",};let { a, b } = o;
これにより、o.a
とo.b
から新しい変数a
とb
が作成されます。c
は必要ない場合はスキップできることに注意してください。
配列デストラクチャリングと同様に、宣言なしで代入することもできます。
ts
({ a, b } = { a: "baz", b: 101 });
この文を括弧で囲む必要があったことに注意してください。JavaScriptは通常、{
をブロックの開始として解釈します。
オブジェクト内の残りのアイテムを変数に格納するには、...
構文を使用します。
ts
let { a, ...passthrough } = o;let total = passthrough.b + passthrough.c.length;
プロパティの名前変更
プロパティに異なる名前を付けることもできます。
ts
let { a: newName1, b: newName2 } = o;
ここで構文が分かりにくくなります。a: newName1
は「a
をnewName1
として」と解釈できます。方向は左から右で、次のように記述した場合と同じです。
ts
let newName1 = o.a;let newName2 = o.b;
紛らわしいことに、ここでのコロンは型を示すものではありません。型を指定する場合は、デストラクチャリング全体の後にも記述する必要があります。
ts
let { a: newName1, b: newName2 }: { a: string; b: number } = o;
デフォルト値
デフォルト値を使用すると、プロパティが未定義の場合にデフォルト値を指定できます。
ts
function keepWholeObject(wholeObject: { a: string; b?: number }) {let { a, b = 1001 } = wholeObject;}
この例では、b?
はb
がオプションであることを示しているので、undefined
になる可能性があります。keepWholeObject
には、b
が未定義であっても、wholeObject
とプロパティa
とb
の変数が含まれるようになりました。
関数宣言
デストラクチャリングは、関数宣言でも機能します。単純なケースでは、これは簡単です。
ts
type C = { a: string; b?: number };function f({ a, b }: C): void {// ...}
しかし、引数にはデフォルト値を指定することが一般的であり、デストラクチャリングでデフォルト値を正しく設定するのは難しい場合があります。まず、パターンをデフォルト値の前に置く必要があることを覚えておく必要があります。
ts
function f({ a = "", b = 0 } = {}): void {// ...}f();
上記のコードスニペットは、ハンドブックで前述した型推論の例です。
次に、メインイニシャライザではなく、デストラクチャリングされたプロパティでオプションのプロパティのデフォルト値を指定することを覚えておく必要があります。C
はb
をオプションとして定義されていたことを思い出してください。
ts
function f({ a, b = 0 } = { a: "" }): void {// ...}f({ a: "yes" }); // ok, default b = 0f(); // ok, default to { a: "" }, which then defaults b = 0f({}); // error, 'a' is required if you supply an argument
デストラクチャリングは慎重に使用してください。前の例で示したように、最も単純なデストラクチャリング式以外は何でも分かりにくくなります。これは、名前変更、デフォルト値、型注釈を積み重ねることなく、深くネストされたデストラクチャリングでは特に理解するのが難しくなります。デストラクチャリング式を小さくシンプルに保つようにしてください。デストラクチャリングによって生成される代入式を自分で記述することもできます。
スプレッド構文
スプレッド演算子は、デストラクチャリングとは逆のものです。配列を別の配列に、またはオブジェクトを別のオブジェクトに展開できます。例えば
ts
let first = [1, 2];let second = [3, 4];let bothPlus = [0, ...first, ...second, 5];
これにより、bothPlusには[0, 1, 2, 3, 4, 5]
という値が設定されます。スプレッド構文はfirst
とsecond
の浅いコピーを作成します。スプレッド構文によって変更されることはありません。
オブジェクトも展開できます。
ts
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { ...defaults, food: "rich" };
ここで、search
は{ food: "rich", price: "$$", ambiance: "noisy" }
となります。オブジェクトのスプレッド構文は、配列のスプレッド構文よりも複雑です。配列のスプレッド構文と同様に、左から右に処理されますが、結果はオブジェクトのままです。つまり、スプレッドオブジェクトの後にあるプロパティは、前のプロパティを上書きします。そのため、前の例を最後にスプレッドするように変更すると
ts
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { food: "rich", ...defaults };
defaults
のfood
プロパティがfood: "rich"
を上書きします。これはこのケースでは望ましい結果ではありません。
オブジェクトのスプレッド構文にも、他にもいくつかの驚くべき制限があります。まず、オブジェクト自身の列挙可能なプロパティのみが含まれます。基本的に、オブジェクトのインスタンスを展開すると、メソッドが失われます。
ts
class C {p = 12;m() {}}let c = new C();let clone = { ...c };clone.p; // okclone.m(); // error!
第二に、TypeScriptコンパイラは、ジェネリック関数からの型パラメータのスプレッドを許可しません。この機能は、将来の言語バージョンで期待されています。
using
宣言
using
宣言は、Stage 3 Explicit Resource Management提案の一部である、JavaScriptの今後の機能です。using
宣言はconst
宣言と非常によく似ていますが、宣言にバインドされた値のライフタイムと変数のスコープを結び付ける点が異なります。
using
宣言を含むブロックから制御が抜けたとき、宣言された値の[Symbol.dispose]()
メソッドが実行され、その値によるクリーンアップが可能になります。
ts
function f() {using x = new C();doSomethingWith(x);} // `x[Symbol.dispose]()` is called
実行時には、これはおおよそ次のものと同等の効果があります。
ts
function f() {const x = new C();try {doSomethingWith(x);}finally {x[Symbol.dispose]();}}
using
宣言は、ファイルハンドルなどのネイティブ参照を保持するJavaScriptオブジェクトを操作する場合に、メモリリークを回避するのに非常に役立ちます。
ts
{using file = await openFile();file.write(text);doSomethingThatMayThrow();} // `file` is disposed, even if an error is thrown
または、トレースなどのスコープ付き操作でも役立ちます。
ts
function f() {using activity = new TraceActivity("f"); // traces entry into function// ...} // traces exit of function
var
、let
、const
とは異なり、using
宣言はデストラクチャリングをサポートしていません。
null
とundefined
値がnull
またはundefined
になる可能性があることに注意することが重要です。その場合、ブロックの終わりで何も破棄されません。
ts
{using x = b ? new C() : null;// ...}
これは、おおよそ次のものと同等です。
ts
{const x = b ? new C() : null;try {// ...}finally {x?.[Symbol.dispose]();}}
これにより、複雑な分岐や繰り返しを行うことなく、using
宣言を宣言するときに、条件付きでリソースを取得できます。
破棄可能なリソースの定義
生成するクラスまたはオブジェクトが破棄可能であることを示すには、Disposable
インターフェースを実装します。
ts
// from the default lib:interface Disposable {[Symbol.dispose](): void;}// usage:class TraceActivity implements Disposable {readonly name: string;constructor(name: string) {this.name = name;console.log(`Entering: ${name}`);}[Symbol.dispose](): void {console.log(`Exiting: ${name}`);}}function f() {using _activity = new TraceActivity("f");console.log("Hello world!");}f();// prints:// Entering: f// Hello world!// Exiting: f
await using
宣言
非同期で実行する必要があるクリーンアップ処理が必要となるリソースや操作があります。これに対応するため、「明示的なリソース管理」提案ではawait using
宣言も導入されています。明示的なリソース管理
ts
async function f() {await using x = new C();} // `await x[Symbol.asyncDispose]()` is invoked
await using
宣言は、その値の`[Symbol.asyncDispose]()`メソッドを呼び出し、**await**します。これは、データベーストランザクションでのロールバックまたはコミット、またはストレージへの保留中の書き込みをフラッシュするファイルストリームなど、非同期のクリーンアップを可能にします。
await
と同様に、await using
は、非同期関数またはメソッド内、またはモジュールの最上位レベルでのみ使用できます。
非同期的に破棄可能なリソースの定義
using
がDisposable
なオブジェクトに依存するように、await using
はAsyncDisposable
なオブジェクトに依存します。
ts
// from the default lib:interface AsyncDisposable {[Symbol.asyncDispose]: PromiseLike<void>;}// usage:class DatabaseTransaction implements AsyncDisposable {public success = false;private db: Database | undefined;private constructor(db: Database) {this.db = db;}static async create(db: Database) {await db.execAsync("BEGIN TRANSACTION");return new DatabaseTransaction(db);}async [Symbol.asyncDispose]() {if (this.db) {const db = this.db:this.db = undefined;if (this.success) {await db.execAsync("COMMIT TRANSACTION");}else {await db.execAsync("ROLLBACK TRANSACTION");}}}}async function transfer(db: Database, account1: Account, account2: Account, amount: number) {using tx = await DatabaseTransaction.create(db);if (await debitAccount(db, account1, amount)) {await creditAccount(db, account2, amount);}// if an exception is thrown before this line, the transaction will roll backtx.success = true;// now the transaction will commit}
await using
とawait
await using
宣言の一部であるawait
キーワードは、リソースの破棄がawait
されることを示すだけです。値自体をawait
するわけではありません。
ts
{await using x = getResourceSynchronously();} // performs `await x[Symbol.asyncDispose]()`{await using y = await getResourceAsynchronously();} // performs `await y[Symbol.asyncDispose]()`
await using
とreturn
Promise
を最初にawait
せずにPromise
を返す非同期関数でawait using
宣言を使用する場合、この動作には小さな注意点があることに注意することが重要です。
ts
function g() {return Promise.reject("error!");}async function f() {await using x = new C();return g(); // missing an `await`}
返されたPromiseがawait
されていないため、`x`の非同期破棄を`await`している間に実行が一時停止し、返されたPromiseを購読していないため、JavaScriptランタイムが未処理の拒否を報告する可能性があります。ただし、これはawait using
特有の問題ではなく、try..finally
を使用する非同期関数でも発生する可能性があります。
ts
async function f() {try {return g(); // also reports an unhandled rejection}finally {await somethingElse();}}
この状況を回避するには、返された値がPromiseである可能性がある場合は、その返された値をawait
することをお勧めします。
ts
async function f() {await using x = new C();return await g();}
for
およびfor...of
文でのusing
とawait using
using
とawait using
の両方をfor
文で使用できます。
ts
for (using x = getReader(); !x.eof; x.next()) {// ...}
この場合、`x`のライフタイムはfor
文全体にスコープされ、break
、return
、throw
によって制御がループから抜けるか、ループ条件が偽になった場合にのみ破棄されます。
for
文に加えて、両方の宣言をfor...of
文でも使用できます。
ts
function * g() {yield createResource1();yield createResource2();}for (using x of g()) {// ...}
ここでは、`x`はループの各反復の最後に破棄され、次に次の値で再初期化されます。これは、ジェネレータによって1つずつ生成されるリソースを使用する場合に特に役立ちます。
古いランタイムでのusing
とawait using
Symbol.dispose
/Symbol.asyncDispose
の互換性のあるポリフィル(最近のNodeJSのバージョンでデフォルトで提供されているものなど)を使用している限り、古いECMAScriptエディションをターゲットにしている場合でも、using
とawait using
宣言を使用できます。