6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScript Handbook を読む (2. Variable Declarations)

Last updated at Posted at 2017-02-27

前回に続き、 TypeScript Handbook を読んでいく。

  1. Basic Types
  2. Variable Declarations (今ココ)
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Variable Declarations

原文

Variable Declarations

letconst は JavaScript でも比較的新しい変数宣言です。
letvar とよく似ていますが、JavaScript を使う中でよくある "gothas" を避けることができます。
const は変数への再代入を禁止する、let の拡張版です。

var declarations

従来の JavaScript での変数宣言には var キーワードが使われてきました。

JavaScript
var a = 10;

見て分かる通り、a という変数を宣言し、値を 10 に設定しています。

メソッドの中で変数宣言することもできますし、

JavaScript
function f() {
    var message = "Hello, world!";

    return message;
}

他のメソッドから変数にアクセスすることもできます。

JavaScript
function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // 11

上記の例では f のなかで宣言された変数 ag がキャプチャしています。
どこで g が呼び出されても、a の値は f の中で宣言されている a の値に拘束されます。

一度 f() の実行が終了すれば、g() が呼ばれていても a を読み取り/変更することができます。

JavaScript
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 のスコープは他の言語と比べて奇妙なところがあります。

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

    return x;
}

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

変数 xif ブロックの中で 宣言されているにも関わらず、ブロックの外からでもアクセスすることができます。
なぜなら、var で宣言された変数は、変数を宣言した関数、モジュール、名前空間、グローバルスコープの中であれば、それがブロックを含むかどうかによらず、どこからでもアクセスすることができるためです。
これは var スコープ関数スコープ とも呼ばれます。
関数の引数も関数スコープとなります。

悪名高き「変数の巻き上げ」というやつだね

このスコープの規則はある種の間違いを引き起こしがちです。
間違いの元になる問題のひとつは、同じ変数を複数回宣言してもエラーにならないことです。

JavaScript
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

以下のコードを実行すると何が起きるでしょう?

JavaScript
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 ループを抜けた になります。
その時点では i10 になっているため、各関数が 10 を出力するというわけです。

これの回避策としては、各繰り返しで i をキャプチャするために即時関数 (IIFE) を使うことです。

JavaScript
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 と同じように使用できます。

TypeScript
let hello = "Hello!";
JavaScript
var hello = "Hello!";

Block-scoping

let を使用して宣言された変数は レキシカルスコープ (ブロックスコープ) になります。
var で宣言された変数は関数内までスコープが広がるのに対し、ブロックスコープの変数はそれを含むブロック、または for ループ内でしかアクセスできません。

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

    if (input) {
        // ここからでも 'a' にアクセスすることができる
        let b = a + 1;
        return b;
    }

    // エラー。 ここでは 'b' にはアクセスできない
    return b;
}
JavaScript
function f(input) {
    var a = 100;
    if (input) {
        // ここからでも 'a' にアクセスすることができる
        var b = a + 1;
        return b;
    }
    // エラー。 ここでは 'b' にはアクセスできない
    return b;
}

ここでは ab という 2 つの変数を宣言しています。
a のスコープは f 全体になるのに対し、b のスコープは if ブロック内に制限されます。

コンパイル後には var に置き換わるため、あくまでもコンパイル時のチェックである点に注意
(まあそれで充分なんだけど…)

catch 句でも同じルールが適用されます。

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

// エラー。ここでは 'e' にアクセスできない
console.log(e);

ブロックスコープの変数の他の特徴としては、変数宣言の前に読み書きできないことが挙げられます。
変数自体はスコープ内に "存在" するものの、変数宣言までは temporal dead zone に属するものとして扱われます。

TypeScript
a++; // エラー。まだ 'a' は宣言していない。
let a;

また、変数宣言の前でも変数を キャプチャ することはできますが、変数宣言前にメソッドを呼び出すことは禁止されています。
ES2015 をターゲットとしているのであれば実行時にエラーになりますが、今の TypeScript ではコンパイルエラーにはなりません。

TypeScript
function foo() {
    // 'a' をキャプチャすることはできる
    return a;
}

// 不正な 'foo' の呼び出し ('a' の宣言前)
// 実行時にエラーになる
foo();

let a;

temporal dead zones についての詳細は Mozilla Developer Network を参照してください。

Re-declarations and Shadowing

var では何回 変数宣言してもエラーにはなりませんでした。

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

    if (true) {
        var x;
    }
}

上記の例では、すべての x同じ x を参照しています。
これはバグの原因となりかねませんが、let では変数宣言は 1 回だけしか許可されません。

TypeScript
let x = 10;
let x = 20; // エラー。同じスコープでは 'x' を再宣言できない

ブロックスコープの変数同士である必要はありません。

TypeScript
function f(x) {
    let x = 100; // エラー: 引数と重複している
}

function g() {
    let x = 100;
    var x = 100; // エラー: 'x' の宣言を複数持てない
}

ただし、ブロックスコープの変数を関数スコープの変数と一緒に宣言できないというわけではありません。
ブロックスコープの変数を異なるブロックで宣言するだけです。

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

    return x;
}

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

ネストしたスコープ内で新しい変数を宣言することを シャドーイング と呼びます。
これはある種のバグを防ぐものの、別のバグの原因にもなるため、諸刃の剣でもあります。
例えば、前述の sumMatrix 関数を let を使って記述してみましょう。

TypeScript
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;
}
JavaScript
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 宣言のところでキャプチャされた変数の振る舞いについて説明しました。
より直感的に説明すると、スコープが実行されるたびに変数の "環境" が作成され、その環境 (とキャプチャされた変数) はスコープの実行が終わった後も存在し続けるということです。

TypeScript
function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

環境内に city がキャプチャされているため、if ブロックを抜けた後でも変数にアクセスすることができます。

前の方に出てきた setTimeout の例は、for ループの繰り返しごとに変数の場外をキャプチャするために IIFE を使用する必要がありました。
これはつまり、変数をキャプチャするために新しい変数環境を作成していたということです。

ループ処理において、let 宣言はまったく違う振る舞いを見せます。
ループ全体に対して新しい環境を作成するのではなく、繰り返しごと に新しいスコープを作成するのです。
これはまさに IIFE を使用して実現しようとしていたことであり、let 宣言を使用して setTimeout の例を書き直してみましょう。

TypeScript
for (let i = 0; i < 10 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
JavaScript
var _loop_1 = function (i) {
    setTimeout(function () { console.log(i); }, 100 * i);
};
for (var i = 0; i < 10; i++) {
    _loop_1(i);
}

for 文の外で i を宣言してしまうと効果がないので要注意

これの実行結果は以下のようになります。

TypeScript
0
1
2
3
4
5
6
7
8
9

const declarations

他の変数宣言方法として、const 宣言があります。

TypeScript
const numLivesForCat = 9;
JavaScript
var numLivesForCat = 9;

constlet と似ていますが、名前の表す通り、一度束縛した値を変更することはできません。
つまり、let と同じスコープルールに従いますが、変数に再代入することはできないということです。

ただし、参照先の変数が 不変 であるということと混同しないようにしてください。

TypeScript
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

分割代入の一番簡単な例は変数の分割代入です。

TypeScript
let input = [1, 2];
let [first, second] = input;
console.log(first); // 1
console.log(second); // 2
JavaScript
var input = [1, 2];
var first = input[0], second = input[1];
console.log(first); // 1
console.log(second); // 2

この例では firstsecond という 2 つの新しい変数を作成しています。
インデックスを使用しても同じことができますが、こちらの方がより便利です。

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

分割代入は宣言済みの変数に対しても適用可能です。

TypeScript
// 変数の入れ替え
[first, second] = [second, first];
JavaScript
// 変数の入れ替え
_a = [second, first], first = _a[0], second = _a[1];
var _a;

一時変数の宣言が後に来るのが気持ち悪い…

メソッドの引数に適用することもできます。

TypeScript
function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f([1, 2]);
JavaScript
function f(_a) {
    var first = _a[0], second = _a[1];
    console.log(first);
    console.log(second);
}
f([1, 2]);

... を使って変数の残り部分を受け取ることができます。

TypeScript
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]
JavaScript
var _a = [1, 2, 3, 4], first = _a[0], rest = _a.slice(1);
console.log(first); // 1
console.log(rest); // [ 2, 3, 4 ]

もちろん、JavaScript と同じように後続の要素を無視することができます。

TyepScript
let [first] = [1, 2, 3, 4];
console.log(first); // 1
JavaScript
var first = [1, 2, 3, 4][0];
console.log(first); // 1

他の要素を無視することもできます。

TypeScript
let [, second, , fourth] = [1, 2, 3, 4];
JavaScript
var _a = [1, 2, 3, 4], second = _a[1], fourth = _a[3];

Object destructuring

オブジェクトを分割代入することもできます。

TypeScript
let o = {
    a: "foo",
    b: 12,
    c: "bar"
}
let { a, b } = o;
JavaScript
var o = {
    a: "foo",
    b: 12,
    c: "bar"
};
var a = o.a, b = o.b;

この例では o.ao.b から新しい変数 ab を作成しています。
ここでは不要な c を無視しています。

配列の時と同様に、変数宣言なしに代入することも可能です。

TypeScript
({ a, b } = { a: "baz", b: 101 });
JavaScript
(_a = { a: "baz", b: 101 }, a = _a.a, b = _a.b);
var _a;

この時、文を括弧で囲む必要がある点に注意してください。
JavaScript では通常、{ はブロックの始まりとしてパースされるためです。

... を使って残りの要素を集めた変数を作ることもできます。

TypeScript
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
JavaScript
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

別名で分割代入することもできます。

TypeScript
let { a: newName1, b: newName2 } = o;
JavaScript
var newName1 = o.a, newName2 = o.b;

この構文を見て混乱しているかもしれませんが、a: newName1 は “anewName1 とする” という意味です。
左から右に処理するため、自分で書くとしたら以下のようになります。

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

紛らわしいことに、ここでのコロンは型を表して いません
もし型も指定する場合、分割代入全体の後ろに書く必要があります。

TypeScript
let { a, b }: { a: string, b: number } = o;
JavaScript
var a = o.a, b = o.b;

型指定と別名の指定を同時にする場合、以下のようにする。

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

Default values

プロパティが存在しない場合のデフォルト値は以下のように指定します。

TypeScript
function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}
JavaScript
function keepWholeObject(wholeObject) {
    var a = wholeObject.a, _a = wholeObject.b, b = _a === void 0 ? 1001 : _a;
}

この例では keepWholeObjectwholeObject 以外にプロパティ a と (引数に プロパティ b が存在しなくても) プロパティ b を保持することになります。

Function declarations

分割代入はメソッドにも適用できます。

TypeScript
type C = { a: string, b?: number }
function f({ a, b }: C): void {
    // ...
}
JavaScript
function f(_a) {
    var a = _a.a, b = _a.b;
    // ...
}

引数のデフォルト値を指定することはよくありますが、分割代入と一緒にデフォルト値を指定する場合はトリッキーになります。
まず最初に以下のパターンを覚えてください。

TypeScript
function f({ a="", b=0 } = {}): void {
    // ...
}
f();
JavaScript
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();

続いて、分割代入されたプロパティのうち、任意のプロパティにデフォルト値を与える方法を覚えてください。

TypeScript
function f({ a, b = 0 } = { a: "" }): void {
    // ...
}
f({ a: "yes" }); // OK、デフォルト値 b = 0 が使用される
f(); // OK、デフォルト値 { a: "" } が使用され、さらにデフォルト値 b = 0 が使用される
f({}); // エラー、引数を指定する場合には 'a' は必須
JavaScript
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

スプレッド演算子 (...) は分割代入とは逆のもので、配列を他の配列内に、オブジェクトを他のオブジェクト内に展開することができます。

TypeScirpt
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
JavaScript
var first = [1, 2];
var second = [3, 4];
var bothPlus = [0].concat(first, second, [5]);

この例では、bothPlus は [0, 1, 2, 3, 4, 5] となります。
スプレッドは firstsecond のシャローコピーを作成しますが、スプレッドによって変更されることはありません。

オブジェクトを展開することもできます。

TypeScript
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
JavaScript
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" } となります。
オブジェクトの展開は配列の展開よりも複雑です。
配列の展開と同じように左から右に処理していきますが、結果はオブジェクトのままになります。
つまり、前に展開したオブジェクトのプロパティが、より後に展開したオブジェクトで上書きされるということです。
先ほどの例を少し変えてみましょう。

TypeScript
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
JavaScript
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);

こうすると defaultsfood プロパティが food: "rich" を上書きしますが、これは期待する動作ではありません。

オブジェクトの展開にはいくつかの制約があります。
1 つ目の制限は オブジェクト自身の列挙可能なプロパティ のみが展開対象となる点です。
つまり、基本的にオブジェクトインスタンスを展開するとメソッドが失われるということです。

TypeScript
class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // OK
clone.m(); // エラー!

2 つ目の制約は、TypeScript コンパイラではジェネリックメソッドの型引数を展開できないことです。
この機能は将来の言語規格に含まれると思われます。

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?