TypeScriptを学ぶ
TypeScriptの特徴
TypeScriptは、Microsoftにより2012年に初めて発表され、スケーラブルなJavaScriptの上位互換。
スケーラブルな言語は、プロジェクトの規模や開発チームが増えても、うまく機能し続けることができる言語のことで、TypeScriptはその特性から大規模プロジェクトに適している。
トランスパイル
TypeScriptのコードは、さまざまなJavaScriptのバージョンへトランスパイル(変更)できる。
これにより、ブラウザや実行環境の互換性問題を解決する。
静的型付け
変数や関数の引数に型を指定することで、コードの安全性が向上し、バグの早期発見につながる。
function sum(x:number,y:number):number {
return x + y
}
型推論
型注釈が付いていない変数でもコンテキストに基づいて自動的に型を推論します。これにより、開発者は型を明示しなくても安全性が向上します。
構造的部分型システム
TypeScriptは構造的部分型システムを導入。オブジェクトの形状(オブジェクトがどのようなプロパティとメソッドを有しているか)に基づいて型を判断します。したがって、公称型ではなく、構造的部分型に基づいて動作する。
構造的部分型
型の名前ではなく、型が持つプロパティやメソッドの構造に着目し、基本型と部分型の関係性を判断する。
class Shape {
area(): number {
return 0;
}
}
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
上記の例だと、CircleクラスはShapeクラスのareaメソッドを持っており、追加でradiusプロパティを定義しています。extendsキーワードを使用していないにも関わらず、CircleはShapeの部分型として扱われる。これは、CircleクラスがShapeクラスの持つ構造(areaメソッド)を含んでいるためです。
その結果、Shape型の変数にCircle型のインスタンスを代入できる
const shape:Shape = new Circle(10);
ジェネリクス
ジェネリクスをサポートしており、汎用的に再利用可能なコードを記述できる
function identity<T>(args:T):T{
return args
}
高度な型表現
TypeScriptは、高度な型システムを用いて複雑な型を表現できます。これにより、アプリケーションのロジックをより堅牢で表現豊かな形で開発できます。以下は、TypeScriptで利用可能な高度な型表現の例です。
- ユニオン型 : 複数の型のどれかを表すことができます。例えば、初期値がnullの変数を処理する場合、ユニオン型を使用できる。
type NullableString = string | null
- タプル型 : 配列の各要素に異なる型を指定できる。
type Point2D = [number,number,string]
複数の言語パラダイムのサポート
TypeScriptは、オブジェクト指向プログラミング(OOP)と関数型プログラミング(FP)の両方をサポート。
クラスとインターフェイス
TypeScriptはクラスベースのオブジェクト指向プログラミングとインターフェースをサポートしている。
これにより、コードの再利用や継承が容易になり、大規模なプロジェクトを管理する際に役立つ(アーキテクチャを導入できる)
interface Person {
name: string
}
class Employee implements Person {
name: string
constructor(firstName:string, lastName:string){
this.name = name;
}
}
メモリ管理
基本的にJavaScriptと同様にメモリ管理を行います。JavaScriptエンジンがガベージコレクションを用いて、自動的にメモリを解放する。
非同期処理
JavaScriptと同様にイベント駆動型の非同期プログラミングをサポートしている。
シングルスレッドモデル
TypeScript(およびJavaScript)はシングルスレッドモデルを採用している。シングルスレッドモデルは、シンプルで分かりやすいコードを実現し、イベントループと非同期処理で効率的なタスク処理をサポートします。一方で、Web Workersを利用してバックグラウンドで実行されるスレッドを作成し、マルチタスクを実現することもできる。
TypeScriptとエコシステム
言語
JavaScriptの仕様を定義したのがECMAScriptです。ECMAScriptはクライアント、サーバーサイドともに共通のコア機能を指します。ブラウザ関連のJavaScript仕様を定めるのがWHATWGです。
また、XMLの構文をJavaScriptにかけるJSXという言語もある。
型定義ファイル
TypeScriptは型をチェックすることで、プログラムの不具合をチェックできます。
しかし、JavaScriptだけで作られたライブラリには、TypeScriptコンパイラーがチェックの材料にする型情報がついていません。
JavaScript純正のライブラリに型情報を持たせる機能として、TypeScriptには型定義ファイルというものがある。型定義のファイルの多くは、DefinitelyTypedというプロジェクトが公開しています。
実行環境
JavaScriptの実行環境は、ブラウザとサーバーの2種類あります。ブラウザは画面描画を行うコンポーネントとして、レンダリングエンジンを持ちます。レンダリングエンジンには、Blinkやwebkit、Geckoなどがあります。
さらに、レンダリングエンジンの内部にJavaScriptエンジンがあります。JavaScriptコードはこのエンジンで評価され実行されます。JavaScriptエンジンには、v8、SpiederMonkey、JavaScriptCoreがあります。
Chrome,Opera
- JavaScriptエンジン
- V8
- レンダリングエンジン
- Blink
Microsoft Edge
- JavaScriptエンジン
- V8(Chakaraから移行)
- レンダリングエンジン
- Blink(EdgeHTMLから移行)
Firefox
-
JavaScriptエンジン
- SpiderMonkey
-
レンダリングエンジン
- Gecko
-
safari
-
JavaScriptエンジン
- JavaScriptCore
-
レンダリングエンジン
- Webkit
読む
モジュールバンドラー
複数のJavaScriptファイルをひとつのファイルに結合するためのツール。
<問題>
複数のJavaScriptファイルに依存関係がある場合、それをそのままブラウザに読み込ませるには、慎重に読み込み順を指定しないと、アプリケーションが壊れてしまう。この問題を回避するために、モジュールバンドラーを使います。
また、フロントエンドでは、JavaScriptアプリケーションをブラウザにダウンロードさせる必要があります。数多くのファイルからなるアプリケーションは、モジュールバンドラーで1ファイルの方が効率がいい。
モジュールバンドラーをを使うと、CommonJSを採用しているサーバーサイド向けに作られたライブラリをブラウザで使用できるようになる。
タスクランナー
ビルドなどの開発上の手続きを自動化するツールです。複数のビルドタスクを束ねたり、実行順序を調整することができます。
トランスパイラー
あるプログラミング言語で書かれたコードを、別の言語に変換するツールです。トランスパイラーはコンパイラーの一種です。JavaScriptでは、新しいバージョンのJavaScriptから古いバージョンのJavaScriptに変換するトランスパイラーがあります。Babelやswcがこれにあたります。TypeScriptにはtsc(TypeScript compiler)もトランスパイラーです。TypeScript -> JavaScriptに変換します。
TypeScriptは何ではないか?
実行時の高速化・省メモリ化に影響しない
<問題>
- TypeScriptはJavaScriptより高速に実行できる
- TypeScriptはJavaScriptよりメモリの消費量が少ない
逆に
- TypeScriptはJavaScriptより低速
- TypeScriptはJavaScriptよりメモリの消費量が激しい
<結論>
- TypeScriptのランタイムは存在しない
- メモリの消費量もJavaScriptと変わらない(コンパイラは最適化しない)
メモリの消費量もJavaScriptと変わらない(コンパイラは最適化しない)
一般的なコンパイラの仕事は3つ
1.ソースコードの解析、問題のチェック
2.ソースコードを別の言語に変換
3.コードの最適化
- 実行速度が速くなるようにする
- 少ないメモリで動くようにする
- 少ない電力で済むようにする
- 実行ファイルサイズを小さくする
このうち、TypeScriptコンパイラの仕事は、上の二つです。3つ目の最適化はしません。
TypeScriptのコード
const oneDay: number = 60 * 60 * 24;
コンパイル後のコード(型注釈が削除されたのみ)
const oneDay = 60 * 60 * 24;
60 * 60 * 24の式は静的に計算できるものです。コンパイル時に計算して、次のようなJavaScriptを生成しておけば、実行時の余計な計算が不要になります。
const oneDay = 86400;
しかし、TypeScriptはこうした最適化は原則的に行いません。
型推論
TypeScriptには型推論(type inference)と呼ばれる機能があります。
型推論は、コンパイラが型を自動で判別する機能です。
プログラマーは型推論を活用すると、型注釈を省略できるので、コードの記述量を減らせる利点があります。
let x = 1; // let x: number = 1;と同じ意味になる
x = "hello";
Type 'string' is not assignable to type 'number'
型推論と動的型付けの違い
静的型付けは、コンパイル時に型が決定される。
動的型付けは、実行時に型が決定される。
プリミティブ型
JavaScriptはプリミティブ型とオブジェクト型の2つに分類される
イミュータブル特性
JavaScriptのプリミティブ型の1つめの特徴は、値を変更できない点です。つまり、イミュータブルです。
オブジェクト型は値を変更できるため、ミュータブルです。
プロパティを持たない
JavaScriptのプリミティブ型の2つ目の特徴は、基本的にプロパティがない。
プリミティブ型のnullとundefinedにはプロパティがありません。
null.toString(); // エラーになる
ところが、文字列や数値などのプリミティブ型は、プロパティを持ったオブジェクトとして扱えます。
"name".length
このように、プリミティブ型をまるでオブジェクトのように扱えるのはJavaScriptの特徴です。
JavaScriptはオブジェクト型に暗黙的に変換します。この機能はオートボクシング、自動ボックス化と呼ばれます。
少数計算の誤差(初知り)
JavaScriptの少数の計算には誤差が生じる場合があるので要注意です。
0.1+0.2 = 0.3 になって欲しいところですが、計算結果は0.30000000000000004になります。
これは、JavaScriptのバグではありません。
0.1+0.2 === 0.3 // false
number型のIEEE754という規格に準拠していて、その制約によって生じる現象です。
10進数の0.2は有限小数(小数第何位が決まっている)ですが、2進数で表現すると、循環小数となる。
循環小数は小数点以下が無限に続きますが、IEEE754が扱う小数点以下は有限であるため、循環小数は桁の途中で切り捨てられます。その結果、少数の計算に誤差が生じます。
undefinedとnullの違い
意味合いの違い
undefinedとnull大きな括りで「値がない」ことを意味する点は共通です。意味的な違いがあるとするならundefinedは「値が代入されていないため、値がない」、nullは「代入すべき値が存在しないため、値がない」
nullは自然発生しない
undefinedは言語仕様上、プログラマーが明示的に使わなくても、自然に発生してくるものです。
例えば、変数を宣言した時に初期値がなければundefinedが代入します。
let value;
console.log(value);
undefined
一方、nullはプログラマーが意図的に使わない限り発生しません。JavaScriptはnullを提供することがない。一部のDOM系のAPIやライブラリによってはnullを返すこともある。
undefinedは変数
undefinedはグローバルで定義された変数です。そのため、undefinedの値を上書きすることも可能です。
typeOf
undefined === undefined
null !== null
nullのデータ型がobjectになるのは、JavaScriptの歴史的な背景があります。
JSON
オブジェクトプロパティにundefinedを用いた時、そのオブジェクトをJSON化した時に、オブジェクトプロパティは削除されます。
console.log(JSON.stringify({ foo: undefined }));
{}
console.log(JSON.stringify({ foo: null }));
{"foo": null}
symbol型(シンボル型)
JavaScriptのsymbol型はプリミティブ型の一種で、その値が一意になる値です。
シンボルはシンボル名が同じであっても、初期化した場所が違う(参照)とfalseになる
const s1 = Symbol("foo");
const s2 = Symbol("foo");
console.log(s1 === s1);
=>true
console.log(s1 === s2);
=>false
シンボルの用途
JavaScriptの組み込みAPIの下位互換性を壊さずに新たなAPIを追加すること。
JavaScript本体をアップデートするために必要だったデータ型
型強制 暗黙的型変換
JavaScriptでは、型が異なる2つの値に対して、演算子してもエラーにならないことがあります。
"1" - 1; //=> 0
これは、string型の1が暗黙的にnumber型に変換されているためです。
ボックス化
多くの言語では、プリミティブ型はフィールドやメソッドを持ちません。
プリミティブをオブジェクトのように使用するには、オブジェクトに変換する必要があります。
プリミティブ型からオブジェクト型への変換をボックス化と呼びます。
自動ボックス化
JavaScriptでは、プリミティブ型でもフィールドやメソッドを呼び出せます。
const str = "abc";
// オブジェクトのように扱う
str.length; // フィールドの参照
str.toUpperCase(); // メソッド呼び出し
これは、内部的にプリミティブ型の値をオブジェクトに変換しているためです。
ラッパーオブジェクト
JavaScriptの自動ボックス化で変換先となるオブジェクトをラッパーオブジェクト(wrapper object)と呼びます。
undefinedとnullはラッパーオブジェクトがないため、メソッドやフィールドを参照できません。
ラッパーオブジェクトとTypeScriptの型
TypeScriptでは、ラッパーオブジェクトの型も定義されています。
ラッパーオブジェクトの型を使って、型注釈を書くこともできます。
const bool: Boolean = false;
const num: Number = 0;
const str: String = "";
const sym: Symbol = Symbol();
const big: BigInt = 10n;
しかし、ラッパーオブジェクトはプリミティブ型に代入できません
const n1: Number = 0;
const n2: number = n1;
Type 'Number' is not assignable to type 'number'.
'number' is a primitive, but 'Number' is a wrapper object. Prefer using 'number' when possible.
ラッパーオブジェクトは演算子が使えません
const num : Number = 1
num * 2
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
ラッパーオブジェクト型は、そのインターフェイスを満たしたオブジェクトであれば、プリミティブ型の値以外も代入できます。
const boolLike = {
valueOf(): boolean {
return true;
},
};
const bool: Boolean = boolLike;
プリミティブ型の代わりにラッパーオブジェクト型を型注釈で使用するメリットはありません。
リテラル型
TypeScriptではプリミティブ型の特定の値だけを代入可能にする型を表現できます。
そのような型をリテラル型と呼ぶ。
// number型
let x : number
x = 1
let y : 1
y = 100
Type '100' is not assignable to type '1'.
リテラル型として、表現できるプリミティブ型
- boolean(true,false)
- number型の値
- string型の値
一般的にリテラル型はマジックナンバー(定数値をハードコーディングで表現している)やステートの表現に用いられます。その際、ユニオン型と組み合わせることが多い。
any型
TypeScriptのany型はどんな型でも代入を許す型です。
let value: any;
value = 1; // OK
value = "string"; // OK
value = { name: "オブジェクト" }; // OK
any型はコンパイラが型チェックを行いません。実行するとエラーになるようなコードでも、コンパイラーは問題を指摘しません。
// 数値に対して、存在しないメソッドを呼び出している
const str: any = 123;
str.toLowerCase();
// 実行時にエラーとなる(コンパイル時でない)
TypeError: str.toLowerCase is not a function
暗黙のany
型を省略してコンテキストから型を推論できない場合は、TypeScriptは暗黙的に型をany型とします。
引数の型注釈を省略した場合などに起こります。
function hello(name) {
(parameter) name: any
console.log(`Hello, ${name.toUpperCase()}`);
}
hello(1);
name.toUpperCase is not a function
TypeScriptでは暗黙のanyを規制するオプションとして、noImplicitAnyが用意されている。
これをtsconfig.jsonに設定することで、TypeScript型をany型と推論した場合、エラーが発生する。
anyは悪
any型はコンパイラーのチェックを抑制したい時に用いる特別な型です。
any型を濫用すると、型チェックが弱くなり、バグの原因になります。
理由なくanyを使うのは問題ですが、どうしてもanyを使わないとならない場合や、型安全を妥協した上で、まず動くコードを形にすることを優先するといったこともありえます。
チームの熟練度やプロジェクトの方針によります!
プリミティブ以外はすべてオブジェクト
JavaScriptは、プリミティブ型以外の全てはオブジェクトです。
プリミティブ型は値が同じであれば、同一のものと判断できます。オブジェクトはプロパティが同じであっても、インスタンスが異なると同一のものとは判定されません。
const value1 = 123;
const value2 = 123;
console.log(value1 == value2);
true
const object1 = { value: 123 };
const object2 = { value: 123 };
console.log(object1 == object2);
false
オブジェクトリテラル
JavaScriptのオブジェクトリテラルは{}で生成できます。
JavaやPHPなどの言語では、オブジェクトを生成するにはまずクラスを定義し、そのクラスを元に
インスタンスを作るのが普通ですが、JavaScriptはクラス定義がなくてもオブジェクを作ることができます。
オブジェクトの型注釈
TypeScriptでオブジェクトの型注釈は、JavaScriptオブジェクトのような書き方で、オブジェクトプロパティをキーと値の型のペアを書きます。
let box : {width:number;height:number};
box = { width: 1080, height: 720 };
プロパティの区切り文字には、オブジェクトリテラルのようなカンマも使えますが、セミコロンを用いる方が推奨されています。理由は、Prettierがオブジェクトの型注釈を直すときに、カンマをセミコロンに置き換えるためです。
型エイリアスを使った型注釈もできます
type Box = {width:number;height:number};
let box:Box = {width:1080,height:720}
オブジェクトの型注釈には、メソッドの型注釈も書くことができます。
let calculator: {
sum(x:number,y:number): number
}
// 関数構文の書き方もできる
let calculator: {
sum: (x: number, y: number) => number;
};
あとで説明あり。
メソッド構文の型注釈と関数構文の型注釈は、基本的に同じ意味ですが、コンパイラーオプションのstrictFunctionTypesを有効にした場合に関数構文の書き方は、メソッド引数のチェックが双変から共変へと厳格になる。
オブジェクト型の型推論
let box = { width: 1080, height: 720 };
メソッドを持つオブジェクトリテラルは型推論ができますが、引数のみ型注釈が必要です。
let calculator = {
sum(x: number, y: number) {
return x + y;
},
};
calculator;
Record<Keys,Type>
連想配列のようなキーバリューのオブジェクトの型を定義する場合、ユーティリティ型のRecordを使うこともある。
let foo:Record<string,number>;
foo = {"num":32,"num2":32}
**object型**
オブジェクト型も型注釈で使用できる
```ts
let box: object;
box = { width: 1080, height: 720 };
object型の使用はお勧めしない。
1.object型に何のプロパティがあるかの情報がない
2.どんなオブジェクトでも代入可能
オブジェクトの型のreadonlyプロパティ
TypeScriptでは、オブジェクトを読み取り専用にすることができます。
読み取り専用にしたいプロパティはreadonly修飾子をつけます。読み取り専用のプロパティに値を代入使用とするとコンパイルエラーとなる。
let obj: {
readonly foo:number
}
obj = {foo:1}
obj.foo = 2;
readonlyは再帰的ではない
readonlyを指定したプロパティのみが読み取り専用になります。
そのため、readonlyは入れ子になっている場合、その中のオブジェクトのプロパティまでを読み取り専用にしません。つまり、再帰的ものではありません
let obj: {
readonly foo: {
bar: number;
};
};
obj = {
foo: {
bar: 1,
},
};
obj.foo = { bar: 2 };
obj.foo.bar = 2; // コンパイルエラーにはならない
こちらを読み取り専用にするには、各プロパティにreadonlyをつける必要がある。
readonlyはコンパイル時のみ
readonlyはTypeScriptの世界の概念です。つまり、読み取り専用は、コンパイル時のみ適用されます。
トランスパイラーによって、TypeScriptからJavaScriptに変換された後は、readonlyの記述はなくなるため、実行時にエラーを検出できません。
全てのプロパティを読み取り専用にする
TypeScriptでは、読み取り専用にするために全てのプロパティにreadonlyをつける必要がある。
しかし、手間です。そのためのユーティリティ関数としてReadOnlyが準備されています。
let obj: ReadOnly<{
a:number;
b:number;
}>
readonlyとconstの違い
const : 再代入不可の変数宣言(オブジェクトのプロパティは変更化)
readonly : TypeScriptの読み取り専用のプロパティ(オブジェクトの再代入はできる)
2つを組み合わせることで、変更不可のオブジェクトを作成できる
const obj : {readonly num : number}
obj.num = 1 //error
obj = {num:1} //error
オプショナルプロパティ
TypeScriptでは、オブジェクトのプロパティにオプショナルの型付けができます。
プロパティの後ろに?を書きます。
let size: {width?: number}
size = {}
//undefined OK!
size = {width: undefined}
//undefined NG!
size = {width: null}
余剰プロパティチェック(excess property checking)
TypeScriptのオブジェクトの型には余剰プロパティチェックという、追加のチェックが働く場合があります。余剰プロパティチェックとは、オブジェクトの方に存在しないプロパティを持つオブジェクトの代入を禁止する検査です。
let onlyX: {x:number}
onlyX = {x:1};
onlyX = {x:1,y:1}; //コンパイルエラーになる
コンパイルエラーとなる原因は{y:2}が余計だと判断されるからです。こうした余計なプロパティを許さないTypeScriptのチェックが余剰プロパティチェックなのです。
余剰プロパティチェックはオブジェクトリテラルのみ検査
余剰プロパティチェックはオブジェクトのプロパティを厳密にチェックし、コードが型に厳密になるような手助けをします。しかし、余剰プロパティチェックが効くのは、オブジェクトリテラルのみです。
変数の代入はこのチェックは行いません。
const xy: { x: number; y: number } = { x: 1, y: 2 };
let onlyX: { x: number };
onlyX = xy; // OK
変数代入にも余剰プロパティチェックが働いた方が良さそうに思われますが、型の厳密性より、利便性をTypeScriptが優先しているためです。
インデックス型
TypeScriptで、オブジェクトのフィールド名をあえて指定せずに、プロパティのみを指定したい場合があります。その場合はインデック型がおすすめです。プロパティ全てがnumber型であるオブジェクトは次のような型注釈をします。
let obj : {
[K : string] : string
};
フィールド名の表現部分が[K:string]です。このKの部分は型変数です。任意の型変数名にできます。
Kやkeyにするのが一般的。インデックス型のフィールド名の方はnumber,string,symbolが指定できる
let obj: {
[K:string]:number
}
obj = {a:1,b:1}
obj.c = 4
obj["d"] = 5
コンパイラーオプションのnoUncheckedIndexedAcessを有効にした場合、インデックス型では、プロパティに指定した型は自動的にプロパティに指定した型とundefinedのユニオン型になる。
const obj: { [K: string]: number } = { a: 1 };
const b: number | undefined = obj.b;
console.log(b);
undefined
Record
インデックス型はRecord<K,T>ユーティリティ型でも表現できます。
let obj1 : {[K:string]:number}
let obj2 : Record<string,number}
プロトタイプベース
JavaScriptはプロトタイプベースです。
オブジェクトの生成
オブジェクト指向プログラミング(OOP)では、オブジェクトを扱います。
オブジェクトとの生成方法は言語によって異なり、「クラスベース」と「プロトタイプベース」があります。
クラスベース
Java,PHP,Rubyなどのプログラミング言語はクラスベースに分類されます。
オブジェクトの設計図である「クラス」を用いて、オブジェクトを生成します。
クラスベースの世界ではこのオブジェクトをインスタンスと呼びます。
class Button {
constructor(name) {
this.name = name;
}
}
// インスタンスの生成
const dangerousButton = new Button("絶対に押すなよ?");
プロトタイプベースとは
JavaScriptはプロトタイプベースです。プロトタイプベースはクラスのようなものが存在しません。
(あったとしても、クラスはオブジェクトの一種です。)
プロトタイプベースはオブジェクトの素にオブジェクトを生成します。
例えば、JavaScriptでは既存のオブジェクトに対して、Object.create()を実行すると新しいオブジェクトを生成できます。
const button = {
name: "ボタン"
}
const dangerousButton = Object.create(button);
dangerousButton.name = "絶対に押すなよ?";
console.log(button.name);
"ボタン"
console.log(dangerousButton.name);
"絶対に押すなよ?"
上記の例だと異なるオブジェクトを生成できています。
プロトタイプとは日本語で「原型」のことです。プロトタイプベースは単純に言ってしまえば、原型となるオブジェクトをもとにオブジェクトを生成するアプローチ。
継承
JavaScriptの継承は既存のオブジェクトから新規のオブジェクトを生成するときに一緒に行う
const obj= {
count: 0,
countUp() {
this.count++;
},
};
const resettableCounter = Object.create(counter);
resettableCounter.reset = function(){
this.count = 0
}
クラス風に書けるJavaScript
ES2015にclassやextends構文が導入。
近年はObject.createの多用や、無理にプロトタイプベースを意識したコードにする必要がない。
なぜJavaScdriptはプロトタイプベースなのか
JavaScriptの開発には次の要件がありました。
- ブラウザで動く言語で、構文はJava風
- Javaほど大掛かりでないように
- 開発期間は10日
工数削減にプロトタイプベースを選択
object,Object,{}の違い
TypeScriptではオブジェクトの型注釈をするとき、プロパティの型まで指定する必要がある
let obj : {a : number,b:number}
プロパティの型を指定せず、ざっくり「オブジェクトであること」を型注釈で指定する方法もある。
let a : object
let b : Object
let c : {};
これはどれもオブジェクトの型の値ならどんなものでも代入可能になる。
object型、Object型、{}型の違い
object型とObject型、{}は異なる点があります。
object型
object型はオブジェクトだけを代入できる型です。JavaScriptのデータ型はプリミティブ型とオブジェクト型の2つに分かれるため、object型はプリミティブ型を代入できない型。
let a: object;
a = { x: 1 }; // OK
a = [1, 2, 3]; // OK。配列はオブジェクト
a = /a-z/; // OK。正規表現はオブジェクト
// プリミティブ型はNG
a = 1;
a = true;
a = "string";
Object型
Object型はインターフェースです。valueOfなどのプロパティを持つ値なら何でも代入できます。したがって、Object型はnullやundefinedを除くあらゆるプリミティブ型も代入できます。string型やnumber型などのプリミティブ型は自動ボックス化(プリミティブ型->ラッパーオブジェクト)により、オブジェクトのプロパティを持てるためです。
オブジェクトの分割代入
JavaScriptには、オブジェクトの分割代入があります。これは、オブジェクトのプロパティを再定義する方法です。
基本の構文は割愛
代入する変数名を指定
const color = { r: 0, g: 122, b: 204, a: 1 };
const {r: red,g: green,b:blue,a:alpha} = color
console.log(red);
//0
オプショナルチェーン
JavaScriptのオプショナルチェーン?.は、オブジェクトの参照がnullやundefinedの場合でも、エラーを起こさずにプロパティを参照できる安全な方法です。
?.に先行する変数やプロパティの値がnull,undefinedの場合、その先のプロパティは評価されずにundefinedを返す。
const book = null;
console.log(book?.title);
undefined
関数の呼び出し
(初知り)
関数を呼び出す時もオプショナルチェーンが使える。
const increment = (n) => n + 1;
const result = increment?.(1);
console.log(result);
配列の参照
配列の要素にも使用できる。[]の前に?.をつける
const books = undefined;
const title = books?.[0];
TypeScriptでの型
TypeScriptでオプショナルチェーンを使った場合、得られる値の型は、最後のプロパティの型とundefinedになる。
オブジェクトのループ
基本的には、Object.entriesを使用する。
const foo = { a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(foo)) {
console.log(key, value);
// a 1
// b 2
// c 3 の順で出力される
}
for inはプロトタイプオブジェクトまで参照するため、バグを見つけにくい。
TypeScriptと構造的型付け
型システムとは
プログラム内のさまざまな値や変数に「型」を割り当てる決まりを指します。
TypeScriptは構造的型付けを採用している。
型の区別に関する2つのアプローチ
- 構造的型付け
- 名前的型付け
名前的型付け
型の名前に基づいて型の区別を行う方法です。このアプローチでは、型の名前が同一かどうかを判断するために、型の名前が重要な役割を果たします。例えば、string型とnumber型は名前が異なるため、異なる形になります。このアプローチはPerson型とDog型は名前が異なるため、異なる型として扱います。
主にJava,PHP,C#,Swiftなどが挙げられます。
class Person {}
class Dog {}
class Main {
public static void main(String[] args) {
Person person = new Person();
Dog dog = person; // コンパイルエラー: 不適合な型
}
}
名前的型付は型の名前を見て、型の一致や互換性を判断している。
構造的型付け
型が持つプロパティやメソッドが同一であれば、異なる名前を持つ型同士でも互換性があると言える。
class Person {
walk() {}
}
class Dog {
walk() {}
}
これらのクラスは異なりますが、クラスの構造は同一です。構造的型付けの観点からは、互換性があると判断できます。
TypeScriptのコードはこちらです。
const person = new Person()
const dog: Dog = person
部分型
多くのプログラミング言語では、型と型との関係性を階層関係で捉えることができます。
階層構造において、頂点に位置するのは最も抽象的な型です。
階層を下に進むほど具体的な形に分かれていきます。
階層構造の上位に位置するのが基本型。階層構造の下位に位置するのが、部分型と呼びます。
基本型は抽象的、部分型は具象的になります。
名前的部分型
名前的型付を採用している場合、型の階層関係を定義する際に、型の名前とその関係性に重点を置きます。このアプローチでは、クラスやインターフェースの継承を通じて、型間の親子関係が形成されます。
名前的型付けのアプローチで扱われる部分型のことを名前的部分型と呼びます。
例えば、Javaだと継承を使用して、基本型と部分型の関係性を宣言します。
この宣言により、特定のクラスが別のクラスの部分型であることをJavaコンパイラに知らせます。
class Shape{}
class Circle extends Shape{}
このコード例は、CircleクラスがShapeクラスを継承しています。この継承により、CircleクラスがShapeクラスの部分型になります。この階層関係により、Shape型の変数にCircle型のインスタンスを代入することができます。
一方で、CircleとShape間にextendsキーワードによる継承関係が宣言されていない場合、両者の間には、階層関係は存在しません。
class Shape{}
class Circle{}
構造的部分型
型間の階層構造もその構造に基づいて判断される。
このアプローチは、型の名前ではなく、型が持つプロパティやメソッドの構造に着目して、基本型と部分型の関係性を判断します。
class Shape {
area(): number {
return 0;
}
}
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
const shape: Shape = new Circle(10);
Cicleクラスは、Shapeクラスのareaメソッドを持っているため、部分型として扱われます。
TypeScriptでもextendsキーワードを用いてクラス間の継承を宣言できます。
しかし、部分型を判断するための基準ではなく、親クラスのインターフェイスを守ることができます。
ダックタイピング
オブジェクトの型よりもオブジェクトの持つメソッドやプロパティが何であるかによってオブジェクトを判断するプログラミングスタイル。ダックタイピングの世界では、特定のインターフェイスをimplementsキーワードを使うなどして明示的に実装する必要はありません。代わりに、オブジェクトが特定の規約に従っているか、例えば、特定のメソッドを持っているかという基準で、そのオブジェクトを判断します。
ダックタイピングでは、型の判断をするために型の名前を使わないのが一般的です。
ダックタイピングは、動的型付け言語によく見られます。JavaScriptも動的型付け言語あり、ダックタイピングとともに歩んできた歴史があります。TypeScriptはJavaScriptの延長線上にある言語です。そのため、ダックタイピングが行えるような型システムが求められてきました。
構造的型付の注意点
意図せずに型の互換性が生じる可能性がある。
構造的型付システムでは、型の互換性はその構造に基づいて判断されます。
このため、異なる目的や意味合いを持つ型が、偶然同じ構造を持っている場合に、意図せずに互換性があると判断されることがあります。
class UserId {
id: string;
}
class ProductId {
id: string;
}
const userId: UserId = new UserId();
const productId: ProductId = userId; // 代入できるが、意図した設計ではない
データモデルやドメインモデルの観点から、ユーザーのIDと商品のIDは全く異なる概念です。
名前的型付けを実現する方法
1.privateのメンバーを持つ
TypeScriptでは、privateメンバーを持つクラスは、他のクラスと区別されます。privateメンバーがそのクラス固有のものであるため、異なるクラスのインスタンス同士は、構造が同じであっても互換性がないと見なされます!
class UserId {
private id: string;
constructor(id: string) {
this.id = id;
}
getId(): string {
return this.id;
}
}
class ProductId {
private id: string;
constructor(id: string) {
this.id = id;
}
getId(): string {
return this.id;
}
}
const userId: UserId = new UserId("1");
const productId: ProductId = userId; // 代入エラー
- ブランド型
ブランド型または幽霊型は型を区別するためのプロパティを形に持たせることで、その型を明確に区別するデザインパターンです。型にメタデータのようなタグをつけることで、構造が同じであっても型と型を区別することができます
interface UserId {
--brand: "UserId",
id:number
}
interface ProductId {
__brand: "ProductId";
id: number;
}
これにより、両者が構造的に同じであっても、型システム上は異なる型として扱われます。
ブランド型で用いられる__brandプロパティは、型を区別するためのものであり、実行時のデータとして持たせる必要はありません。これを達成するために、__brandプロパティはasキーワードを使って型アサーションを行う手法がよく使われます。
読み取り専用の配列
TypeScriptでは配列を読み取り専用として型注釈できます。
型注釈の方法は2つあります。
1.readonly
2.ReadonlyArray<T>
readonly
配列の型注釈T[]の前にreadonlyキーワードを添えると、読み取り専用の配列型にできます。
const nums: readonly number[] = [1,2,3]
ReadonlyArray<T>
number型の配列を読み取り専用にしたい場合、ReadonlyArray<number>と書きます。
const nums: ReadonlyArray<number> = [1, 2, 3];
2つの違い
readonly T[]とReadonlyArray<T>の違いは書き方以外にありません。書き手で決める。
読み取り専用配列の特徴
読み取り専用の配列には、破壊的な操作(push,pop,reverse)が、コンパイル時には無いことになります。
const nums: readonly number[] = [1, 2, 3];
nums.push(4); // コンパイル時にエラーになる(Property 'push' does not exist on type 'readonly number[]'.)
これは、破壊的操作のメソッドを呼び出そうとするコードがTypeScriptコンパイラーに警告されるだけです。配列オブジェクトからpushメソッドを削除しているわけではありません。なので、JavaScript実行時にpushメソッドを実行できます。
const nums: readonly number[] = [1, 2, 3];
console.log("push" in nums); // numsオブジェクトにpushプロパティ名が存在するか
// true
そのため、コンパイルエラーを無視して実行してみると、読み取り専用の型でも配列を書き換えることができる
const nums: readonly number[] = [1, 2, 3];
// @ts-ignore
nums.push(4); // 本来コンパイルエラーになるが無視する
console.log(nums);
読み取り専用配列を配列に代入する
TypeScriptの読み取り専用配列を普通の配列に代入することはできません。代入しようとするとコンパイルエラーになります。
const readOnlyNum : readonly number[] = [1,2,3]
const writableNum: number[] = readonlyNumbers; //代入できません
普通の配列は破壊的操作のメソッドを保持していますが、読み取り専用配列にはそれが無いことになっているためです。どうしても読み取り専用の配列を代入するには、型アサーションを使う方法があります。
const readonlyNumbers: readonly number[] = [1, 2, 3];
const writableNumbers: number[] = readonlyNumbers as number[];
配列の共変性(covariance)
共変とはその型自身、もしくは、その部分型が代入できることを言います。
例えば、Animal型とDog型があります。Dog型はAnimal型の部分型とします。
interface Animal {
isAnimal: boolean
}
interface Dog extends Animal {
isDog: boolean
}
let pochi:Dog = { isAnimal: true, isDog: true };
let animal: Animal = pochi // 代入OK
一方で共変では、Dogの変数には、DogのスーパータイプであるAnimalは代入できません。
let animal: Animal = { isAnimal: true };
let pochi: Dog = animal;
配列は共変が許される
TypeScriptの配列は共変が許されている。Anima型にDog型を代入できる(現状、バグの原因になると感じている)
interface Animal {
isAnimal: boolean;
}
interface Dog extends Animal {
wanwan(): string; // メソッド
}
const pochi = {
isAnimal: true,
wanwan() {
return "wanwan"; // メソッドの実装
},
};
const dogs: Dog[] = [pochi];
const animals: Animal[] = dogs;
animals[0] = { isAnimal: true }; // 同時にdogs[0]も書き換わる dog[0]も{isAnimal:true}のみになる
const mayBePochi: Dog = dogs[0];
mayBePochi.wanwan();
// JS実行時エラー: mayBePochi.wanwan is not a function
型の安全性を突き詰めると、配列は共変であるべきでは無いです。
型があるJavaでは、List<T>型は共変ではなく非変になっています。非変な配列では、その型自信しか代入できないようになります。
// Javaコード
import java.util.*;
class Animal {
}
class Dog extends Animal {
}
public class Main {
static {
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs;
// エラー: 不適合な型: List<Dog>をList<Animal>に変換できません
}
}
TypeScriptの配列が共変の理由
健全性、利便性のためです。
共変とは、ある型にその部分型を代入できることです。
下記コードで、TypeScriptが非変であった場合、コンパイルエラーになります。
sumの引数に(number|null)[]を期待しているが、number[]が渡されているためです。
function sum(values: (number | null)[]): number {
let total = 0;
for (const value of values) {
if (typeof value === "number") {
total += value;
}
}
return total;
}
const values: number[] = [1, 2, 3];
sum(values);
これを回避するには余計なアサーションをつける必要があります。
sum(values as (number | null)[]);
// ^^^^^^^^^^^^^^^^^型アサーション
こうしたことが随所で起きると、書くのも読むのも不便になります。従って、TypeScriptは型の完璧さより、利便性を優先しています。
タプル
TypeScriptの関数は1値のみ返却可能です。戻り値に複数の値を返したい時に、配列に返したい全ての値を入れて返すことがあります。
タプルの型
タプルの型は[]を書いて中に型を書くだけです。
function tuple(): [number, string, boolean] {
//...
return [1, "ok", true];
}
const list:[number,string,boolean]= tuple()
タプルへのアクセス
タプルを受けた変数はそのまま中の型が持っているプロパティ、メソッドを使用できます。
const list: [number, string, boolean] = tuple();
list[0].toExponential();
list[1].length;
list[2].valueOf();
タプルの分割代入
上記関数tuple()の戻り値は分割代入を使用できる
const [num,str,bool]: [number,string,boolean] = tuple();
タプルを使う場面
TypeScriptで非同期プログラミングをするときに、時間のかかる処理を直列ではなく、並列で行いたい時があります。そのときTypeScriptでは、Promise.all()というものを使用します。このときタプルが役に立ちます。
const tuple: [string, number] = await Promise.all([
takes3Seconds(),
takes5Seconds(),
]);
Promise.all()は先に終了した関数から戻り値のタプルに格納されることはなく、元々の順番を保持します。
列挙型(enum)
TypeScriptでは、列挙型を用いると、定数のセットに意味を持たせたコード表現ができます。
列挙型を宣言するには、enumキーワードの後に列挙型名とメンバーを書きます。
enum Position{
Top,
Right,
Bottom,
Left
}
列挙名は型として扱うこともできます。
ユニオン型
TypeScriptのユニオン型(union type)は「いずれかの型」を表現するものです。
ユニオン型の注釈
ユニオン型の型注釈は、2つ以上の型をパイプ記号(|)で繋げて書きます。例えば、number型もしくはundefined型を表す場合は、number|undefinedのように書きます。
type ErrorCode =
| 400
| 401
| 402
| 403
| 404
| 405;
配列の要素にユニオン型を使う際の書き方
配列の要素としてユニオン型を用いる場合は、書き方に注意が必要です。例えば、stringまたはnumberからなる配列の型を宣言することを考えてみましょう。
type List = (string | number)[]
判別可能なユニオン型(discriminated union) めっちゃいい
TypeScriptの判別可能なユニオン型は、ユニオンに属する各オブジェクトの型を区別するための「しるし」がついた特別なユニオン型です。オブジェクトの型からユニオン型を絞り込む際に、分岐ロジックが複雑になる場合は、判別可能なユニオン型を使うとコードの可読性と保守性がよくなります。
通常のユニオン型は絞り込みが複雑になる
TypeScriptのユニオン型は自由度が高く、複数の型を組み合わせることができます。
type UploadStatus = InProgress | Success | Failure;
type InProgress = { done: boolean; progress: number };
type Success = { done: boolean };
type Failure = { done: boolean; error: Error };
しかし、関数を実装した場合、Successにはプロパティが存在しないとコンパイルエラーを起こします
function printStatus(status: UploadStatus) {
if (status.done === false) { // statusがSuccessかFailureの可能性がある
console.log(`アップロード中:${status.progress}%`);
}
}
判別可能なユニオン型
TypeScriptの判別可能なユニオン型はユニオン型の応用です。
判別可能なユニオン型の特徴
1.オブジェクトの型で構成されたユニオン型
2.各オブジェクトの型を判別するためのプロパティ(しるし)をつける
このプロパティをディスクリミネータ(discriminator)と呼ぶ
3.ディスクリミネータの方はリテラル型などであること
4.ディスクリミネータさえあれば、オブジェクトは固有のプロパティを保持できる
例えば、上記の例だと
type UploadStatus = InProgress | Success | Failure;
type InProgress = { type: "InProgress"; progress: number };
type Success = { type: "Success" };
type Failure = { type: "Failure"; error: Error };
typeというディスクリミネータが追加されたところです。
判別可能なユニオン型の絞り込み
ディスクリミネータを分岐すると型が絞れる
function printStatus(status: UploadStatus) {
if (status.type === "InProgress") {
console.log(`アップロード中:${status.progress}%`);
} else if (status.type === "Success") {
console.log("アップロード成功", status);
} else if (status.type === "Failure") {
console.log(`アップロード失敗:${status.error.message}`);
} else {
console.log("不正なステータス: ", status);
}
}
判別可能なユニオン型を使った方が、コンパイラーが型の絞り込みを理解できます。その結果、分岐処理が読みやすく、保守性も高まります。
ディスクリミネータに使える型
ディスクリミネータに使える型はリテラル型とnull,undefinedです。
- リテラル型
- 文字列リテラル: 'success','failure'
- 数値リテラル: 1,200
- 論理値リテラル : trueまたはfalse
- null
- undefined
インターセクション型
考え方はユニオン型と相対するものです。ユニオン型がどれかを意味するならインターセクション型はどれもです。
インターセクション型を作るためには合成したオブジェクト同士を&で列挙します。
type TwoDimensionalPoint = {
x: number;
y: number;
};
type Z = {
z: number;
};
type ThreeDimensionalPoint = TwoDimensionalPoint & Z;
const p: ThreeDimensionalPoint = {
x: 0,
y: 1,
z: 2,
};
xyからxyzの点に変更しました。
プリミティブ型のインターセクション
プリミティブ型のインターセクション型を作ることもできますが、作るとneverという型ができます。
type Never = string & number;
const n: Never = "2";
このnever型にはいかなる値も代入できません。使い道がまるで無いように見えますが意外なところで役に立ちます。
インターセクション型を使いこなす
システムの巨大化に伴い、受け付けたいパラメータが巨大化したとします。
type Parameter = {
id: string;
index?: number;
active: boolean;
balance: number;
photo?: string;
age?: number;
surname: string;
givenName: string;
company?: string;
email: string;
phoneNumber?: string;
address?: string;
// ...
};
一見してどのプロパティが必須で、どのプロパティが選択可かが非常にわかりづらいです。
そのときに、インターセクション型とユーティリティクラスのRequired<T>とPartial<T>を使いわかりやすく記述できます。
type Mandatory = Required<{
id: string;
active: boolean;
balance: number;
surname: string;
givenName: string;
email: string;
}>;
type Optional = Partial<{
index: number;
photo: string;
age: number;
company: string;
phoneNumber: string;
address: string;
}>;
type Parameter = Mandatory & Optional;
型エイリアス
TypeScriptで名前の追加型を型エイリアスと呼びます。
型エイリアスの宣言
型エイリアスを宣言するにはtypeキーワードを使います。
type StringOrNumber = string | number
型エイリアスの使い道
型エイリアスは同じかたを再利用したいときに使うと便利です。型の定義が一箇所になるため、保守性が高まります。
型アサーション「as」
TypeScriptには、型推論を上書きする機能があります。その機能を型アサーションと言います。
TypeScriptのコンパイラーはコードをヒントに型を推論してくれます。その型推論は非常に知的ですが、場合によってはコンパイラーよりもプログラマーがより正確な型を知っている場合があります。
型アサーションの書き方
型アサーションの書き方は2つあります。
// as構文
const value: string | number = "this is a string"
const strLength: number = (value as string).length
//アングルブラケット構文
const value: string | number = "this is a string"
const strLength: number = (<string>value).length;
アングルブラケット構文はJSXと見分けがつかないため、as構文を使用することが多い
コンパイルエラーになる型アサーション
片アサーションを使えば制限なく型の情報を上書きできるかといるとそうではない。
number型をstring型にする型アサーションはコンパイルエラーになる
const num = 123;
const str: string = num as string;
number型をstring型にするのは間違いです。お互いの方に共通する部分が少ないためです。
それでも自分の書いた型がアサーションが正しいという場合は、unknown型を経由することで上のエラーを回避できます
const num = 123;
const str: string = num as unknown as string; // OK
型アサーションとキャストの違い
キャストとは、実行時にデータ型を別の型に変更することです。
しかし、型アサーションは型を変換しません。あくまでコンパイル時にコンパイラーに型を伝えるだけです。実行時に型を変換するには、そのためのロジックが必要です。
大いなる力には大いなる責任が伴う
基本的にはコンパイラーが提示する型推論を使用する。
型アサーションはやむ得ない場合に使用する。
型アサーションを使う必要が出たら、型ガードやユーザー定義ガードで解決できないかを検討してみてください。
constアサーション「as const」
オブジェクトリテラルの末尾にas constをつけると、readonlyでリテラルタイプで指定したものと同等の扱いができます。
readonlyとconst assertionの違い
どちらのオブジェクトのプラパティをreadonlyにする機能は同じ
readonlyはプロパティごとにつけられる
const assersionはオブジェクト全体、readonlyはプロパティのみにつけることができる
const assertionは再帰的にreadonlyにできる
オブジェクトの中にオブジェクトがあるときの挙動が異なります。
// readonlyの場合
type Country = {
name: string;
capitalCity: string;
};
type Continent = {
readonly name: string;
readonly canada: Country;
readonly us: Country;
readonly mexico: Country;
};
const america: Continent = {
name: "North American Continent",
canada: {
name: "Republic of Canada",
capitalCity: "Ottawa",
},
us: {
name: "United States of America",
capitalCity: "Washington, D.C.",
},
mexico: {
name: "United Mexican States",
capitalCity: "Mexico City",
},
};
// Continentのタイプエイリアスが持つプロパティはすべてreadonlyのため、下記の代入はできません。
america.name = "African Continent";
america.canada = {
name: "Republic of Côte d'Ivoire",
capitalCity: "Yamoussoukro",
};
// しかし、オブジェクトのプロパティには代入できる
america.canada.name = "Republic of Côte d'Ivoire";
america.canada.capitalCity = "Yamoussoukro";
const assertionは全てのプロパティを固定する
as constをつけます
const america = {
name: "North American Continent",
canada: {
name: "Republic of Canada",
capitalCity: "Ottawa",
},
us: {
name: "United States of America",
capitalCity: "Washington, D.C.",
},
mexico: {
name: "United Mexican States",
capitalCity: "Mexico City",
},
} as const;
// 代入できない
america.canada.name = "Republic of Côte d'Ivoire";
america.canada.capitalCity = "Yamoussoukro";
明確な割り当てアサーション
明確な割り当てアサーションは、変数やプロパティが確実に初期化されていることをコンパイラに伝える演算子。
strictNullChecksと変数の初期化エラー
TypeScriptはコンパイラオプションstrictNullChecksがtrueのとき、初期化されていない変数を参照したときにエラーになる。
let num:number
console.log(num* 2)
Variable 'num' is used before being assigned.
変数の初期化が明らかに関数内で行われている場合でも、コンパイラは変数が初期化されていないとエラーになります。
let num : number;
initNum();
console.log(num * 2)
function initNum() {
num = 2
}
strictPropertyInitializationとプロパティの初期化エラー
- strictNullChecks
- strictPropertyInitialization
の両方がtrueの時、クラスのプロパティが初期化されていないとエラーを出します。
TypeScriptのコンパイラは、constructorで初期化することのみを見ています。
constructor以外のメソッドで初期化するところまでは追いかけません。
class Foo {
num1: number = 1; // 初期化している
num2: number;
num3: number;
constructor() {
this.num2 = 1; // 初期化している
this.initNum3(); // num3を初期化している
}
initNum3() {
this.num3 = 1;
}
}
明確な割り当てアサーションを使う
変数やプロパティの初期化が確実に行われていることをコンパイラに伝えるには、明確な割り当てアサーションを使います。
let num!:number
initNum();
console.log(num * 2) //エラーにならない
function initNum() {
num = 2
}
classの場合
class Foo {
num!: number;
// ^明確な割り当てアサーション
}
非Nullアサーション
変数を参照するコードにて、変数の後に!を書きます。
let num:number
initNum();
console.log(num! * 2) //エラーにならない
function initNum() {
num = 2
}
より安全なコードを書くには
明確な割り当てアサーションと非Nullアサーションは、コンパイラから人間へと型の安全性を保証するチェックを移すものです。人間は、コンパイラよりミスします。こうしたアサーションはできるだけ使わずに、型ガードを使うことをお勧めします。
let num: number | undefined;
initNum();
if(typeof num === 'number'){
num * 2
}
function initNum() {
num = 2;
}
変数のスコープ
スコープとは、変数がどこから参照できるかを定めた範囲です。JavaScriptには大きくグローバルスコープとローカルスコープの2つがあります。
グローバルスコープ
グローバルスコープはプログラムのどこからでも参照できる変数です。JavaScriptにはグローバルオブジェクトと呼ばれるオブジェクトが存在します。ブラウザでは、windowオブジェクトがあります。
グローバル変数は、グローバルオブジェクトのプロパティです。ブラウザでは、windowを初略できます。
ローカルスコープ
一定の範囲にだけ効く変数スコープです。
-
関数スコープ
関数内で参照できる変数 -
レキシカルスコープ
関数を定義した場所の外から参照できる変数
const x = 100
function a() {
console.log(x)
}
a();
- ブロックスコープ
ブロックスコープは{}(ブレース)で囲まれた範囲だけ有効なスコープ。ブロックスコープないの変数は、ブロック外から参照できません。
{
const x = 100
console.log(x)
}
console.log(x)
ブロックスコープはif文などのブレースに作用します。条件文の中で定義された変数は、条件文外から参照できません。
意図しないグローバル変数の代入
JavaScriptでは、変数宣言時にletやconstを書き忘れた場合は、グローバルな変数になります。
しかし、TypeScriptでは、変数宣言時にletやconstを書き忘れるとコンパイルエラーになります。
for-of 文
JavaScriptで配列をループする際に使用するfor-of文。
for(const 変数名 of 配列){
}
for-ofでインデックスを取得
const words = ["a","b","c"]
for(const [index,word] of words.entries()){
console.log(index , word)
}
switchと変数スコープ
switchごとにスコープが作成されます。
switch (
true // 変数スコープその1
) {
default:
switch (
true // 変数スコープその2
) {
default:
// ...
}
}
caseは、共通のスコープです。そのため、同名の変数を再定義できません。
let x = 1;
switch (x) {
case 1:
const sameName = "A";
break;
case 2:
const sameName = "B";
break;
}
caseに変数スコープを作る方法
{}ブレースで囲む
switch(x){
case 1: {
}
case2: {
}
}
例外処理
例外はErrorオブジェクトを使い、throw構文で例外を投げます。
throw構文
JavaScriptはthrowは例外を投げる構文です。例外として投げるオブジェクトはErrorオブジェクトを使うのが一般的です。
throw new Error("throw err")
しかし、throwはなんでも投げられます。しかし、Errorオブジェクトを使用するのが一般的です。
Errorオブジェクトを使用した方が、読み手に意外性を与えないのと、スタックトレースが追えるためです。
never型
値を持たないを意味する型
neverの特性
値を代入できない
const foo: never = 1
const a : never = 1 as any
const foo2: never = 2 as neverは再代入できる
何にでも代入できる
const nev = 1 as never;
const a: string = nev; // 代入OK
const b: string[] = nev; // 代入OK
値がない
戻り値の存在しない。例えば、例外が必ず発生する関数の戻り値や無限ループがnever型に当たります。
function throwError(): never {
throw new Error();
}
function forever(): never {
while (true) {} // 無限ループ
}
あとは、型エイリアスでstring型とnumber型のインターセクション型を定義した場合もnever型になります。
type NumberString = number & string;
neverを使った網羅チェック
never型の何も代入できない特性を用いて、網羅チェックに応用ができます。
網羅チェックとは、ユニオン型の分岐処理をするときに、ロジックが全てのパターンを網羅しているかをコンパイラにチェックさせることを言います。
type Extension = "js" | "ts" | "json";
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
// "json"に対する分岐がない
}
}
例外による網羅チェック
例外用の網羅性チェック用の例外クラスを定義することがお勧め。このクラスは、コンストラクタにnever型を取ります。
clsss ExhaustiveError extends Error {
constructor(value:never,message = `Unsupported type: ${value}`){
super(message)
}
}
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default:
throw new ExhaustiveError(ext);
}
}
この例外をdefaultで投げるようにします。こうしておくことで、網羅性が満たされない場合、TypeScriptのエラーになります。
また、コンパイル後も
class ExhaustiveError extends Error {
constructor(value, message = `Unsupported type: ${value}`) {
super(message);
}
}
function func(value) {
switch (value) {
case "yes":
console.log("YES");
break;
case "no":
console.log("NO");
break;
default:
throw new ExhaustiveError(value);
}
}