変数の宣言

変数の宣言

letconstは、比較的新しい形式のJavaScriptの変数宣言になります。 前の章で述べたように、letvarに近しいものですが、 開発者がJavaScriptを動かす際に、"はまりやすい"問題を避ける手助けとなってくれます。 constletを強力にしたものであり、 変数への値の再割当てを禁止します。

TypeScriptはJavaScriptのスーパーセットであるため、 当然letconstを言語としてサポートします。 ここで、これらの新しい宣言が、何故varよりも推奨されるのかを詳しく説明します。

もし、あなたがJavaScriptを粗野に扱っているのであれば、 次のセクションはあなたの考えをリフレッシュする良い機会になるかもしれません。 もし、JavaScriptのvar宣言の癖を完璧に理解しているのであれば、 次のセクションはスキップしても構いません。

var宣言

JavaScriptにおける変数の宣言には、常にvarキーワードが使用されてきました。

var a = 10;

ご覧のとおり、これは10の値のaという変数を宣言しているだけです。
また、関数内部に変数を宣言することも可能です。

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(); // returns '11'

上記の例でgには、fの内部で宣言された変数が割り当てられています。 どの時点であれ、gが呼び出されると、aの値はf内のaの値に紐付けられます。 fの実行が完了したすぐ後にgが呼び出れさたとしても、 aのアクセスと変更は可能になります。

スコープの規則

var宣言は、他の言語のそれと比べて幾つか奇妙なスコープの規則を持ちます。 下記の例をご覧ください。

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

一部の読者は、この例に驚いてもう一度見なおしてしまうかもしれません。 一般的には、変数xifブロック内で宣言されたのであれば、 外部のブロックからは、これにアクセスすることは出来ないでしょう。

これはvar宣言が、含まれる関数、モジュール、名前空間、グローバルスコープ、 これら全ての含まれるブロックに関係なく、後からどこからでもアクセス可能であるためです。 一部の人はこれをvarスコープ(var-scoping)、または関数スコープ(function-scoping)と呼びます。 引数もまた関数にスコープされます。

これらのスコープ規則が、ある類のミスを招きます。 実際に事態を悪化させる要因に、複数回同じ変数を宣言してもエラーにならないことが挙げられます。

function sumMatrix(matrix: number[][]) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) { //iを宣言
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) { //ここでもiを宣言
            sum += currentRow[i];
        }
    }

    return sum;
}

iは同じ関数内のスコープの変数を参照するため、内側のforループは意図せずi変数を上書きしてしまいます。 経験豊富な開発者であればご存知だと思いますが、 このような種のバグはコードレビューをすり抜け、我々のフラストレーションの原因になることがあります。

奇妙な変数への割り当て

下記のスニペットが何を出力するのが、即答してみてください。

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

先程説明した変数割り当てのことを思い出してください。

どの時点であれ、gが呼び出されると、aの値はf内のaの値に紐付けられます。

この状況について少し考えてみましょう。 setTimeoutは数秒後に実行されますが、 これはforループが実行を停止した後でもあります。 forループの実行が完了すると、iの値は10になります。 そのため、与えられた関数が実行されるタイミングでは、出力される値は10になります。

一般的な解決策として、IIEF(Immediately Invoked Function Expression - 即時実行関数)を使用して、 各繰り返し時にiを割り当ててしまいます。

for (var i = 0; i < 10; i++) {
    // 現在の値で関数を実行することで、現在の'i'の状態を割り当てます。
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

奇妙なパターンに見えますが、実際にはよく見かけるものです。 引数のiは実際にはforループ内で宣言されたiに紐付きますが、 同じ名前を付けているため、ループの本文を変更する必要はありませんでした。

let宣言

ここまでで、あなたはvarが持つ幾つかの問題と、 変数を定義する新しい方法であるlet文が何故必要かについて理解したものと思います。 使用方法はさておき、let文はvar文と同じ方法で書かれます。

let hello = "Hello!";

違いの核心は文法ではなく、その意味付け(semantics)にあります。 これから、その核心について触れていきます。

ブロックのスコープ

letを使用して宣言された変数は、「レキシカルスコープ」または「ブロックスコープ」と呼ばれるものを使用します。 varで宣言された変数のスコープは、それが含まれる関数外に漏れてしまいますが、 letのブロックスコープの変数は直近のブロックまたはforループの外からアクセスすることは出来ません。

function f(input: boolean) {
    let a = 100;

    if (input) {
        // aの参照はOK
        let b = a + 1;
        return b;
    }

    // エラー: bはここには存在しない
    return b;
}

ここに、abの2つのローカル変数があります。 aのスコープはfの本文内に限定され、 一方bのスコープはif文のブロック内に限定されます。

catch節の中で宣言された変数も、同様のスコープ規則を持ちます。

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// エラー: 'e'はここには存在しない
console.log(e);

ブロックスコープされた変数のその他の特徴として、実際に変数が宣言される前には、 読み込み・書き込みが出来ないことが挙げられます。 そういった変数がスコープで"提示"されている区間は、宣言されるまでは一時的なデッドゾーンとなってしまいます。 端的に言えば、let文の前ではアクセス出来ないということですが、 幸いにもTypeScriptはそのことを伝えてくれます。

a++; // illegal to use 'a' before it's declared;
let a;

ただし、宣言される前にブロックスコープされた変数にアクセス出来てしまうケースがひとつだけ存在するので注意が必要です。 それは、宣言前の非合法な関数の呼び出しです。 もしES2015が対象であれば、最新のランタイムはエラーをスローしますが、 現状のTypeScriptはこれを許可し、エラーを報告しません。

function foo() {
    // 'a'へのアクセス可能
    return a;
}

// 非合法な宣言前の'foo'の呼び出し
// ランタイムはここでエラーをスローするべき
foo();

let a;

一時的なデッドゾーンの詳細については、 Mozilla Developer Networkの関連記事を参照してください。

再宣言とシャドーイング

var宣言については、前述したとおり同じ変数を何回宣言しても問題にはなりません。

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

上記の例では、全てのxの宣言は実際には同じxを参照し、全く問題のない有効な処理文です。 ただし、これがバグの発生源になることが度々あります。 幸いなことに、let宣言はこれを許しません。

let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

TypeScriptが問題があることを我々に伝えるために、必ずしも両方の変数がブロックスコープされる必要はありません。

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

ブロックスコープされた変数が、関数スコープされた変数で宣言出来ないというわけではありません。 ブロックスコープされた変数は、明確に異なるブロックで宣言される必要があるということです。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false, 0); // returns '0'
f(true, 0);  // returns '100'

より深いスコープで新しい名前が割り当てられることを、シャドーイング(shadowing)と呼びます。 これは特定のバグを防ぐ一方で、シャドーイング自身のイベントで、意図しない特定のバグを生んでしまう可能性があるという点で、 諸刃の刃の要素を持ちます。 例えば、先の例で書いた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;
}

ここでのループは内部ループのiが外部ループからシャドー(shadow)されるため、 実際に正しく動作します。

綺麗なコードを保つために、シャドーイングの使用は通常であれば避けるべきです。 何らかの状況でこれが相応しいケースもあるかもしれないので、それはあなたの判断のもとに使用されるべきでしょう。

ブロックスコープされた変数の捕捉

我々はまず初めに、var宣言された変数の振る舞いについて、 その変数がどのように捕捉されるのかについて学習しました。 これをより直感的に言い表すと、スコープが実行される度に変数の「環境」を作り出している、と言えます。 その環境と補足された変数は、そのスコープ内の実行が完了した後も存在し続けることができます。

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宣言を使用したものに変更してみましょう。

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は、これまでの変数宣言とは異なるものです。

const numLivesForCat = 9;

let宣言のようですが、一度値が割り当てられると、それを変更することはできません。 言い換えると、letと同じスコープ規則を持ちますが、それらへの再割当てはできません。

参照値が不変(immutable)であるという考え方と混同しないでください。

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変数の内部の状態の変更は可能なままです。

let vs const

我々には同様のスコープ付けを持つ2つの種類の宣言が与えられましたが、 どちらを使用するかは当然自分たちで決める必要があります。 このような質問に関する答えは、決まって「場合による」です。

「権限を最小にする」という信念に従うのであれば、変更する予定の無い全ての宣言にはconstを使用するべきです。 基本的には、もし変数への書き込みが不要だった場合、 同じコード上で他の動作がオブジェクトへの自動的な書き込みをするべきではないのであれば、 その変数への再割当てが本当に必要かどうかを考慮する必要があるでしょう。 constを使用することで、データのフローを判断する際にコードがより予測しやすいものになります。

一方で、letがあれば、もはやvarを使用する理由はなく、 多くのユーザーはこの簡潔さを気に入ることでしょう。 このハンドブックの大半は、この恩恵を受けるためにlet宣言を使用しています。

あなたのベストな判断に従い、可能であるならば、チームの残りのメンバーとこのことについて話合ってみてください。

分割代入

TypeScriptが持つECMAScript2015の別の機能に分割代入(destructuring)があります。 詳細については、Mozilla Developer Networkの記事を参照してください。 このセクションでは、手短かに概要を説明します。

配列の分割代入

分割代入の最もシンプルな形式は、配列の分割代入です。

let input = [1, 2];
let [first, second] = input;
console.log(first);  // 1を出力
console.log(second); // 2を出力

これはfirstsecondという2つの新しい変数を作成しています。 これはインデックスを使用した場合と同じですが、それに比べるとはるかに便利でしょう。

first = input[0];
second = input[1];

既に宣言された変数への代入分割も同様です。

// 変数の交換
[first, second] = [second, first];

また、関数への引数としても使用できます。

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f(input);

...name文法を使用して、リスト内の残りの項目のための変数を作成することができます。

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1 を出力
console.log(rest);  // [ 2, 3, 4 ] を出力

これはJavaScriptであるため、当然残りの要素を気にせずに無視することも可能です。

let [first] = [1, 2, 3, 4];
console.log(first); // 1 を出力

他の要素のために次のようにすることも可能です。

let [, second, , fourth] = [1, 2, 3, 4];

オブジェクトへの分割代入

オブジェクトへの分割代入も可能です。

let o = {
    a: "foo",
    b: 12,
    c: "bar"
}
let {a, b} = o;

これは、o.ao.bから、新しい変数であるabを作成します。 必要が無ければcをスキップすることが可能です。

配列の分割代入のように、宣言無しで割り当てることも可能です。

({a, b} = {a: "baz", b: 101});

この文を括弧で囲んでいることに注目してください。 JavaScriptは通常、ブロックの始まりとして{を解析します。

...の構文を使用して、オブジェクト内の残りの項目の変数を作成することができます。

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

プロパティの改名

また、プロパティに異なる名前を割り当てることも可能です。

let {a: newName1, b: newName2} = o;

文法が紛らわしくなってきましたね。 これは、a: newName1は「anewName1とする」と読むことができます。 左から右への書き方をするとすれば、下記と同義になります。

let newName1 = o.a;
let newName2 = o.b;

紛らわしいですが、ここでのコロンは型を示すものではありません。 もし型を指定したいのであれば、現状ではまだ分割代入の後に書く必要があります。

let {a, b}: {a: string, b: number} = o;

デフォルト値

デフォルト値は、プロパティが未定義(undefined)の場合にデフォルトの値を指定するものです。

function keepWholeObject(wholeObject: {a: string, b?: number}) {
    let {a, b = 1001} = wholeObject;
}

ここでは、keepWholeObjectbundefinedであっても、 変数wholeObjectaだけでなく、abを持ちます。

関数宣言

また、分割代入は関数の宣言でも動作します。 分かりやすいように、単純な例を見てみましょう。

type C = {a: string, b?: number}
function f({a, b}: C): void {
    // ...
}

ただし、引数のためのデフォルト値を指定することはより一般的であり、 分割代入によってデフォルト値を右辺より取得するのは、扱いづらい傾向があるでしょう。 まず第一に、デフォルト値を割り当てる前に、型を指定することを忘れないようにする必要があります。

function f({a, b} = {a: "", b: 0}): void {
    // ...
}
f(); // ok, default to {a: "", b: 0}

次に初期化の処理の代わりとなるプロパティへの分割代入で、 任意のプロパティに対してデフォルト値を指定することを覚えておく必要があります。 Cbを任意プロパティとして定義していたことを思い出してください。

function f({a, b = 0} = {a: ""}): void {
    // ...
}
f({a: "yes"}) // OK, bにはデフォルト値0を代入
f()           // OK, デフォルトは{a: ""}となり、bにはデフォルト値0を代入
f({})         // エラー, 引数を指定するのであれば'a'は必須

分割代入は注意して使用するようにしてください。 前述した例のように、分割代入の式はシンプルなどころか厄介なケースが数多く存在します。 特に、これは入れ子の深い分割代入のケースで当てはまり、 改名、デフォルト値、型アノテーションが無かったとしても解析するのは本当に困難です。

分割代入の式は、小さくシンプルにすることを心がけてください。

スプレッド(Spread)演算子

スプレッド(Spread)演算子は分割代入とは正反対のものです。 これは、配列を別の配列へ、またはオブジェクトを別のオブジェクトへ、広げるような(展開する)ことを可能にしてくれます。 下記はその例になります。(訳注: ...がスプレッド演算子)

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

bothPlusには[0, 1, 2, 3, 4, 5]の値が割り当てられます。 スプレッド(spread)すると、firstsecondのシャローコピーが作成されます。 firstsecondは、スプレッドによって変更されません。

また、オブジェクトをスプレッドすることも可能です。

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

searchは、{ food: "rich", price: "$$", ambiance: "noisy" }になります。 オブジェクトのスプレッドは、配列よりも複雑です。

配列のスプレッドと同様に左から右に進みますが、あくまでもオブジェクトのままです。 つまり、スプレッドのオブジェクトの後に来るプロパティは、先に来たプロパティを上書きします。 そのため、末尾にスプレッドが来るように先程の例を修正すると、

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

defaultfoodプロパティは、food: "rich"を上書きし、我々が望む結果とは異なる結果になります。

オブジェクトのスプレッドには他にも驚くべき制限がいくつかあります。 1つ目はそれが自身の列挙可能なプロパティのみを含むということです。 要するに、オブジェクトのインスタンスをスプレッドすると、メソッドを失うこということです。

class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

2つ目はTypeScriptのコンパイラが、ジェネリクス関数からの型パラメーターのスプレッドを許さないことです。 この機能は、将来のバージョンでサポートされる可能性があります。

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。