- TypeScript のインストール
- tsc コマンドでトランスパイルを実行
- ECMAScript のバージョンを指定してトランスパイルする
- ECMAScript 6 compatibility table
- 型注釈と型推論
- 変数に型を与える
- 関数に型を与える
- 複雑な型を扱う
- コンパイラを制御する
- Class を TypeScript で書く
- 継承先に特定のメソッドの実装を強制する(Abstract クラス)
- シングルトンパターンの実装
- オブジェクトの型を定義する(interface)
- プロパティを追加できるオブジェクトの型を定義する(インデックスシグネチャ)
- 在っても無くても良いオプショナルプロパティを持つオブジェクトの型を定義する
- 在っても無くても良いオプショナルパラメータ(引数)を持つ関数を定義する
- TypeScript の構造的部分型
- 型A の全てのプロパティかつ型B の全てのプロパティを持つ intersection 型(交差型)
- 条件文を使って型を絞り込む(Type guard)
- 型を強制的に上書きする(型アサーション)
- null や undefined ではないことをコンパイラに伝える(! 演算子)
- プロパティの参照がエラーにならないようにする(?. 演算子)
- null や undefined のときに初期値を返す(?? 演算子)
- 引数や戻り値が異なる同名の関数を定義する(オーバーロード)
- 型を取得する
- 型の互換性について
- interface を使った関数の型の表現
- Literal 型の Widening / NonWidening を理解する
TypeScript のインストール
Typescript は開発時にのみ必要なので、–save-dev を付けてインストール。
$ npm init
$ npm install --save-dev typescript
インストールしたバージョンの確認。
$ npx tsc --version
Version 4.2.3
tsc コマンドでトランスパイルを実行
このインストールにより、コマンドライン端末(ターミナル)で「tsc」コマンドが使えるようになっている。このコマンドで TypeScript から JavaScript への変換ができる。
実際に hello.ts という TypeScript で書かれたファイルを以下のように作成し、tsc コマンドを実行してみる。(TypeScript で書かれたファイルの拡張子は .ts)
hello.ts
const message: string = "Hello world!";
console.log(message);
$ npx tsc hello.ts
すると、hello.js というファイルが作成される。
hello.js
var message = "Hello world!";
console.log(message);
当然、このコンパイルされたファイルは node.js 上で実行することもできる。
$ node hello.js
Hello world!
ちなみに、VSCode などで hello.ts, hello.js の両方のファイルを開いているとエラー「ブロックスコープの変数 ‘message’ を再宣言することはできません」と表示される。このエラーは片方のファイルを閉じるか、あるいは次のようにブロックスコープに閉じ込めるようにすれば解消する。
hello.ts
{
const message: string = "Hello world!";
console.log(message);
}
ECMAScript のバージョンを指定してトランスパイルする
–target オプションを使って、トランスパイル後のECMAScript のバージョンを指定することができる。指定しなかった場合は、デフォルトで ES3 としてトランスパイルされる。
// person.ts
class Person {}
バージョンを指定しない場合
npx tsc person.ts
// person.js
var Person = /** @class */ (function () {
function Person() {
}
return Person;
}());
ES2015(ES6)を指定した場合
npx tsc person --target es6
// あるいは npx tsc person --target es2015
class Person {
}
ECMAScript 6 compatibility table
TypeScript で記述したコードを、ECMAScript のバージョンを指定してトランスパイルすることができることを上で述べた。しかし、コードで使用するオブジェクトによっては指定したバージョンに変換できないものも存在する。
例えば、ECMAScript 6 compatibility table の Built-ins の Proxy の項目を見ると、TypeScript では(Babel であっても)ES5 にトランスパイルできないことが分かる。
型注釈と型推論
型注釈とは
TypeScirpt では、変数がどのようなデータを格納する目的のものであるかをコンパイラーに明示するために、変数の宣言時に型注釈(タイプアノテーション)を付与することができる。
例えば、TypeScript では、変数の宣言を
let val: number = 10;
let str: string = 'Hello world!';
などのように記述し、このときの「: number」や「: string」の部分のことを、型注釈(タイプアノテーション)と呼んでいる。
型推論とは
TypeScript のコンパイラーは、変数宣言時に型注釈(タイプアノテーション)を省略した場合でも、初期値の型から変数の型を推論している。
型注釈による明示的な型の指定をしなくても、コンパイラーが型推論してくれるため、型推論が働かないときのみ型注釈を付ける、とした方が冗長な記述を防ぐことができる。
変数に型を与える
boolean 型(真偽型)の変数を宣言する
let val: boolean = true;
ただし、初期値から型推論が働くため、通常は型注釈を書かない。
number 型(整数、浮動小数点数)の変数を宣言する
TypeScript では JavaScript と同様に、整数でも浮動小数点数でもどちらも number 型で扱う。
let val: number = 10;
let val2: number = 3.14
let val3: number = -3.14
ただし、初期値から型推論が働くため、通常は型注釈を書かない。
string 型(文字列型)の変数を宣言する
let str: string = 'Hello world!'; // シングルクウォート
let str2: string = "Hello world!"; // ダブルクウォート
let str3: string = `Hello world!`; // バッククオート
ただし、初期値から型推論が働くため、通常は型注釈を書かない。
オブジェクトに型を付ける方法(後述の interface も参照)
{
name: 'John',
age: 32
}
このようなオブジェクトを格納する変数の型注釈(タイプアノテーション)は次のように行う。
const person: {
name: string;
age: number;
} = {
name: 'John',
age: 32
}
型注釈の各プロパティの末尾は、カンマ「,」ではなく、セミコロン「;」であることに注意。
オブジェクトがネスト(入れ子)されている場合も同様に型注釈できる。
const person: {
name: {
firstName: string;
lastName: string;
};
age: number;
} = {
name: {
firstName: 'John',
lastName: 'Travolta'
},
age: 32
}
ただし、初期値から型推論が働くため、通常は型注釈を書かない。
配列に型を付ける方法
配列の要素の型が全て同じである場合
const animals: string[] = ['dog', 'cat', 'rabbit'];
型注釈の角括弧 [] は配列を、string[] は文字列の配列であることを表現している。
要素の型の並びが固定の場合(タプル型)
配列の0番目は string、1番目は number、2番目は boolean というように、要素の型の並びを固定した配列というものを TypeScript では扱うことができる。これをタプル型(Tuple)と呼ぶ。
タプル型の配列を宣言する場合は、配列を表す角括弧 [] の中に要素の型を並べることで型注釈を行う。
const person: [string, number, boolean] = ['John', 32, true];
タプル型の配列は、型推論が正しく機能しないため、必ず型注釈を行う必要がある。
また、TypeScript は変数への初期値の代入は厳しくチェックするが、その後の破壊的なメソッドによる要素の変更までは追跡してチェックしたりはしないので注意が必要。
const person: [string, number, boolean] = ['John', 32, true];
person.shift();
person.push(100);
console.log(person); // [32, true, 100]
複数の型が混在している場合(共用型)
複数の型が混在している配列の場合は、共用型(Union Types)を使って型注釈をすることもできる。
const animals: (string | number)[] = ['dog', 'cat', 'rabbit', 100];
複数の型を代入できる変数を宣言する(共用型)
共用型(Union Types)を使って型注釈をすることで、複数の型を代入できる変数を宣言することができる。
let data: string | number = 'Hello world';
data = 100;
data = true; // error
特定の1つの値のみを代入できる変数を宣言する(Literal 型)
型注釈に 100, ‘Hello’ などの値を指定することで、その値しか代入できない変数を宣言することができる。
let str: 'Hello' = 'Hello';
str = 'world'; // error
let pi: 3.14 = 3.14;
pi = 3.14;
pi = 3; // error
このサンプルのように宣言した変数 str には、文字列 ‘Hello’ しか代入できない。それは結局のところ定数を定義しているのと同じであり、また、実際に const を使って定義した変数は、リテラル型で型推論される。
ただし、const で定義した変数の型推論によるリテラル型は Widening Literal Types と呼ばれ、普通のリテラル型とは少し異なる振る舞いをする。(後述)
特定のグループの値だけを代入できるようにする
列挙型を使用する方法
TypeScript では、定数をひとまとめにした型を定義することができ、列挙型(Enum)と呼ばれる。
enum Color {
RED = 'red',
GREEN = 'green',
BLUE = 'blue'
}
let c: Color = Color.BLUE;
c = 100; // Type '100' is not assignable to type 'Color'
Enum は変数に制約を設けるための型という側面と、変数に代入する値という側面がある。コンパイル後の JavaScrip ファイルを覗いてみると(TypeScript がエラーを吐いていても、JavaScript としては実行可能なコードのため、コンパイル済みのファイルは作成される)、次のようになっている。
var Color;
(function (Color) {
Color["RED"] = "red";
Color["GREEN"] = "green";
Color["BLUE"] = "blue";
})(Color || (Color = {}));
var c = Color.BLUE;
c = 100;
つまり enum で定義した上記のコードは次のようなオブジェクトを生成していることが分かる。
var Color = {
RED: "red",
GREEN: "green",
BLUE: "blue"
}
このため、変数に代入する値として Color.BLUE を使用することができる。
ただし、Enum 型の互換性(後述)が複雑であり、上記のコードを以下の様に変更するとエラーを吐かなくなる。
enum Color {
RED = 'red',
GREEN = 'green',
BLUE = 10 // number 型の値に変更
}
let c: Color = Color.BLUE;
c = 100;
Literal 型と共用型を使用する方法
Literal 型と共用型を組み合わせることでも、特定のグループの値のみを代入できる変数を定義することができる。
let c: 'red' | 'green' | 'blue' = 'blue';
c = 'red'
c = 'green';
c = 'hello'; // error
ただし、注意点として、このように宣言した型を持つ変数を他の変数に代入したとき、次のように型推論では1つのリテラルのみを許容する型と見なされてしまう。
let c: 'red' | 'green' | 'blue' = 'blue';
let cc = c; // "blue" 型(リテラル型)と型推論される
cc = 'red'; // error
このような場合は型推論に任せずに明示的に型注釈をしてやればよい。
let c: 'red' | 'green' | 'blue' = 'blue';
let cc: 'red' | 'green' | 'blue' = c;
cc = 'red'; // ok
さらに type エイリアス(後述)を使用すれば、もう少しシンプルに記述できる。
type Color = 'red' | 'green' | 'blue';
let c: Color = 'blue';
let cc: Color = c
cc = 'red';
型のチェックが行われない変数を宣言する(any 型)
any 型の変数は、どんな型の値でも代入でき、また、他のどんな型の変数にも代入できるという特徴がある。つまり、TypeScript による型のチェックを働かせない変数を宣言することができる。
let a: any = 100;
a = 'Hello';
a = true;
let str: string = 'Hello world';
str = a;
console.log(str); //true
値を代入する時は型チェックを行わないが、参照するときは型ガードが必要な変数を宣言する(unknown 型)
unknown 型の変数は、any 型と同様にどんな型の値でも代入できるが、参照するときには、コードによる型のチェック(型ガードと呼ばれる)が必要になる。
つまり、unknow 型の変数には何が入っているか分からない(unknown)から、その値を利用する時はコードで値の型のチェックをする必要がある、ということを意味している。
unknown 型は「型安全な any 型」とも呼ばれる。
let an: any ='Hello';
let un: unknown = 'Hello';
an = 100; // コンパイルエラーにならない(なんでも代入できる)
un = 100; // コンパイルエラーにならない(なんでも代入できる)
// console.log(an.substr(1)); // コンパイルエラーにならず、実行時にエラー
// console.log(un.substr(1)); // コンパイルエラー
if (typeof un === 'string') {
console.log(un.substr(1)); // string 型として特定された後はコンパイルエラーにならない
}
読み取り専用の型を付ける(readonly)
const arr: number[] = [1, 2, 3, 4, 5];
arr.push(6);
console.log(arr);
// [ 1, 2, 3, 4, 5, 6 ]
const roArr: readonly number[] = [1, 2, 3, 4, 5];
roArr.push(6);
// error: Property 'push' does not exist on type 'readonly number[]'.
const person: [string, number, boolean] = ['Tom', 32, true];
person[0] = 'John';
console.log(person);
// [ 'John', 32, true ]
const roPerson: readonly [string, number, boolean] = ['Tom', 32, true];
roPerson[0] = 'John';
// error: Cannot assign to '0' because it is a read-only property.
関数に型を与える
関数に型注釈をつける方法
関数では、引数と戻り値に型注釈を付ける
関数の場合は次の例のように、引数と戻り値に対して型注釈を与える。
function add(n1: number, n2: number): number {
return n1 + n2;
}
引数では「n1: number」のように、仮引数の後ろにコロンを置き、スペースを入れてから型を記述する。
戻り値では、引数の丸括弧 ( ) の後に同様にして型を記述する。
関数宣言文の引数は型推論できない(any 型と推論される)
関数宣言文の引数に型注釈を付けなかったときは、その引数は any 型と推論される。
戻り値が無い関数に型をつける
関数に戻り値が無いことをコンパイラーに示すためには、戻り値の型注釈で void 型を使用する。
function func(): void {
console.log('Hello world!');
}
JavaScript では関数に戻り値が無い(return 文を省略、あるいは return; とした場合)でも、実際には undefined という値が返却される。
TypeScirpt にも undefined 型があるため、戻り値の型注釈には undefined 型を指定すべきように思えるが、TypeScript では関数に戻り値が無いことを示すための型として void 型というものを用意している。
したがって、一般的には void 型を使用して、関数に戻り値が無いことを示すようにして、明示的に undefined を返却する関数であることを示す必要があるときのみ、戻り値の型注釈に undefined 型を使用するようにする。
関数を格納する変数に型を付ける
下の画像は、関数宣言文で定義した関数 calcArea を変数 calcFn に代入した時の型推論を表している。
この画像からも分かるように、関数を格納する変数の型は
(w: number, h: number) => number
のように表すことができる。戻り値の型注釈の部分がコロン「:」ではなくアロー「=>」で記述されていることに注意。また引数名は何でもよい。
この例では、型推論が正しく働くため、変数 calcFn に型注釈を付けるのは冗長だが、あえて型注釈を記述するなら次のようになる。
function calcArea(width: number, height: number): number {
return width * height;
}
const calcFn: (w: number, h: number) => number = calcArea;
関数式に型を付ける
関数式は、関数の定義と変数への代入を1つの式で行える構文だが、関数式では、
- 左辺の変数に型注釈を行う
- 右辺の関数に型注釈を行う
のとちらか一方を行えばよい。他方は型推論が働く。
const calcArea: (w: number, h: number) => number = function(width, height) {
return width * height;
};
const calcArea = function(width: number, height: number): number {
return width * height;
};
アロー関数に型を付ける
左辺の変数に型注釈を付ける場合
const squareNum: (n: number) => number = n => n * n;
右辺の関数に型注釈を付ける場合
const squareNum = (n: number): number => n * n;
メソッドに型を付ける
オブジェクトのメソッドに型を付ける方法は、次のように2通り存在する。(type キーワードについては後述)
type User = {
name: string;
age: number;
greeting: (msg: string) => void;
greeting2(msg: string): void;
}
const user: User = {
name: 'Tom',
age: 32,
greeting(msg: string) {
console.log(msg);
},
greeting2(msg: string) {
console.log(msg);
}
}
return が実行されない・終点に到達しない関数に型を付ける(never型)
常に例外を投げたり、無限ループであるなどの理由で return が実行されない・終点に到達しない関数の戻り値は、never 型で型注釈を行う。
function error(message: string): never {
throw new Error(message);
}
関数の残余引数(Rest Parameter)に型を付ける
残余引数(Rest Parameter)は渡された引数を配列で受け取るためのものなので、残余引数(Rest Parameter)に指定する型には、配列またはタプルを使用する。
function myfunc(n1: number, n2: number, ...restArgs: number[]) {
console.log(n1, n2, restArgs);
}
myfunc(1, 2, 3, 4, 5, 6, 7);
// 1 2 [ 3, 4, 5, 6, 7 ]
タプルを指定すると、タプルで指定した型・個数の引数に制限される。
function myfunc(n1: number, n2: number, ...restArgs: [string, number, boolean]) {
console.log(n1, n2, restArgs);
}
myfunc(1, 2, 'Hello', 100, true);
// 1 2 [ 'Hello', 100, true ]
myfunc(1, 2, 'Hello', 100, true, 1000);
// error: Expected 5 arguments, but got 6.
タプルを指定するときは、オプションのパラメータも指定できる。
function myfunc(n1: number, n2: number, ...restArgs: [string, number?, boolean?]) {
console.log(n1, n2, restArgs);
}
myfunc(1, 2, 'Hello', 100, true);
// 1 2 [ 'Hello', 100, true ]
myfunc(1, 2, 'Hello');
// 1 2 [ 'Hello' ]
タプルでは次のような指定もできる。
function myfunc(n1: number, n2: number, ...restArgs: [string, number, ...number[]]) {
console.log(n1, n2, restArgs);
}
myfunc(1, 2, 'Hello', 100, 200, 201, 202);
// 1 2 [ 'Hello', 100, 200, 201, 202 ]
複雑な型を扱う
型に別名を与えて複雑な型をシンプルに表現する(type エイリアス)
「type」キーワードを使用することで型にエイリアス(別名)を与えることができる。
type Color = 'red' | 'green' | 'blue';
let c: Color = 'blue';
c = 'red';
c = 'hello'; // error
コンパイラを制御する
ファイルを保存したら自動でコンパイルさせる(watch モード)
コマンドライン端末(ターミナル)で、.ts ファイルを指定して「tsc」コマンドを実行すると .ts ファイルがコンパイルされることは最初に説明した。
$ npx tsc hello.ts
このとき、「-w」オプション、あるいは「–watch」オプションを指定することで、コンパイラが watch モードで動作するようになり、指定ファイルを変更して保存する度に自動でコンパイルが行われるようになる。
$ npx tsc hello.ts --watch
watch モードを終了する場合は、「Ctrl + C」を入力すればよい。
設定ファイル(tsconfig.json)を作成する(tsc –init)
TypeScript のコンパイラの動作を制御するための設定ファイルである tsconfig.json を作成するには、プロジェクトのディレクトリで 「tsc –init」を実行する。
$ npx tsc --init
このコマンドを実行すると、tsconfig.json ファイルが作成される。また、このファイルが作成された後では、ファイルを指定しないで tsc コマンドを実行すると、コンパイラは tsconfig.json の設定内容を読み込み、全ての .ts ファイルをコンパイルするようになる。(ファイルをコンパイル対象から除外する方法は後述)
$ tsc --watch
また、ファイルを指定して tsc コマンドを実行した場合は、tsconfig.json の設定はそのファイルのコンパイルに適用されないことに注意が必要だ。
# ファイルを指定した場合は、tsconfig.json の設定は適用されないので注意。
$ tsc hello.ts
コンパイル対象のファイルを指定する
コンパイル対象から除外するファイルを指定する(exclude)
tsconfig.json
{
"compilerOptions": {
...
},
"exclude": [
"src/hello.ts",
"node_modules"
]
}
このようにすると、src ディレクトリの hello.ts や node_modules ディレクトリはコンパイルされなくなる。(node_modules のコンパイル対象外指定については後述する。)
また、ワイルドカード(*)(**)も使用することができる。
{
"compilerOptions": {
...
},
"exclude": [
"src/hello.ts",
"*.spec.ts",
"**/sample.ts",
"node_modules"
]
}
このようにすると、拡張子「.spec.ts」を持つルートディレクトリのすべてのファイルや、全てのディレクトリの sample.ts がコンパイルされなくなる。
node_modules をコンパイル対象から除外する
“exclude” を指定しないときは、デフォルトで node_modules はコンパイル対象外になっている。しかし、“exclude” を指定すると node_modules もコンパイル対象になってしまうため、明示的に mode_module を指定する必要がある。
{
"compilerOptions": {
...
},
"exclude": [
...
"node_modules"
]
}
コンパイル対象のファイルを指定する(include)
“include” を指定しない(かつ後述の “files” を指定しない)場合は、デフォルトで全ての .ts ファイルがコンパイル対象となる。
“include” を次のように設定すると、ルートディレクトリの index.ts と src ディレクトリの sample.ts だけがコンパイル対象のファイルとなる。
{
"compilerOptions": {
...
},
"include": [
"index.ts",
"src/sample.ts"
]
}
“inclue” と “exclude” に同じものを指定した場合は “exclude” が優先される。
コンパイル対象のファイルを指定する(files)
“inclue” と “exclude” で同じものを指定した場合は “exclude” が優先される。しかし、”files” で同じものを指定すると、”files” が優先され、コンパイルされることになる。
“files” ではワイルドカードは使用することができない。コンパイル対象としてディレクトリを指定することもできない。指定できるのはファイルだけなので注意。
{
"compilerOptions": {
...
},
"exclude": [
"**/sample.ts",
"node_modules"
],
"files": [
"tmp/sample.ts"
]
}
コンパイラオプション(compilerOptions)
変換後の ECMAScript のバージョンを指定する(target)
ECMAScript のバージョンは、compilerOptions の target で指定することができる。指定しなかった場合は、ES3 でコンパイルされる。
{
"compilerOptions": {
...
"target": "es5",
...
}
}
ブラウザで TypeScript のコードをデバッグする(soureMap)
“sourcMap” に true を指定することで、コンパイル時にマップファイルが作成され、ブラウザで TypeScript のファイルをデバッグすることができるようになる。
tsconfig.json
{
"compilerOptions": {
...
"sourceMap": true,
...
}
}
例えば、index.html と sample.ts を次のように作成する。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>sourceMap</title>
</head>
<body>
<script src="sample.js"></script>
</body>
</html>
sample.ts
const message = 'Hello world!';
const greeting = (msg: string): void => {
console.log(msg);
}
greeting(message);
この時、tsconfig.json において、”sourcMap” に true を指定してコンパイルすると、sample.js の他に、次のような sample.js.map というマップファイルも作成される。
sample.js.map
{"version":3,"file":"sample.js","sourceRoot":"","sources":["sample.ts"],"names":[],"mappings":";AAAA,IAAM,OAAO,GAAG,cAAc,CAAC;AAE/B,IAAM,QAAQ,GAAG,UAAC,GAAW;IAC3B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACnB,CAAC,CAAA;AAED,QAAQ,CAAC,OAAO,CAAC,CAAC"}
そして index.html をブラウザで開き、開発者ツールの「Sources」タブを開くと、次のように TypeScript のコードでデバッグすることができる。
Class を TypeScript で書く
class を定義すると型も作られる
TypeScript では、Class を定義することは次の2つの意味を持つ。
- オブジェクトの設計図を作成する。
- Class から生成されるインスタンスの型を作成する。
つまり、TypeScript では型も同時に作られている。メソッドの中で使用されている this の型を指定したい場合は、メソッドの第1引数に this の型を記述し、第2引数以降に本来のメソッドの引数を記述する。
class User {
name: string;
constructor(name: string) {
this.name = name;
}
greeting(this: User) {
console.log(`Hello! My name is ${this.name}`);
}
greeting2(msg: string) {
console.log(`${msg} My name is ${this.name}`);
}
greeting3(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
}
}
const tom = new User('Tom');
tom.greeting(); // Hello! My name is Tom
tom.greeting2('Bye!'); // Bye! My name is Tom
tom.greeting3('ByeBye!'); // ByeBye! My name is Tom
public 修飾子と private 修飾子
プロパティやメソッドに何の修飾子も書かなければ、デフォルトで public になる。もちろん明示的に書くこともできる。
class User {
private name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public greeting(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
console.log(`I am ${this.age} years old.`);
}
}
const tom = new User('Tom', 32);
tom.greeting('Hello');
プロパティの宣言を省略する
上記のサンプルは constructor の引数の部分にプロパティの宣言をまとめることで、次のように書くこともできる。
class User {
constructor(private name: string, private age: number) {
this.name = name;
this.age = age;
}
public greeting(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
console.log(`I am ${this.age} years old.`);
}
}
const tom = new User('Tom', 32);
tom.greeting('Hello');
constructor 内の初期化処理を省略して記述する
さらに、上記サンプルは、constructor 内の記述を次のように省略して記述することができる。
class User {
constructor(private name: string, private age: number) {
}
public greeting(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
console.log(`I am ${this.age} years old.`);
}
}
const tom = new User('Tom', 32);
tom.greeting('Hello');
プロパティを読み出し専用にする(readonly 修飾子)
readonly 修飾子を使うことでプロパティを読み出し専用にすることができる。このとき、class 内部のメソッドでも書き換え禁止になる。ただし、constructor 内では書き換え可能。
class User {
constructor(private readonly name: string, private readonly age: number) {
}
public greeting(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
console.log(`I am ${this.age} years old.`);
}
}
const tom = new User('Tom', 32);
tom.greeting('Hello');
protected 修飾子で継承先からアクセスできるようにする
TypeScirpt では protected 修飾子も使えるので、継承先のクラス内でも使えるプロパティを定義することができる。
class User {
constructor(protected name: string, protected age: number) {
this.name = name;
this.age = age;
}
public greeting(this: User, msg: string) {
console.log(`${msg} My name is ${this.name}`);
console.log(`I am ${this.age} years old.`);
}
}
class SubUser extends User {
constructor(name: string, age: number, private id: number) {
super(name, age);
this.id = id;
}
userInfo() {
console.log(`name: ${this.name}, age: ${this.age}, id: ${this.id}`);
}
}
const tom = new SubUser('Tom', 32, 1000);
tom.greeting('Hello');
tom.userInfo();
継承先に特定のメソッドの実装を強制する(Abstract クラス)
abstract キーワードを使用することで、継承先のサブクラスで必ず特定のメソッドを実装することを強制することができる。
abstract class Person {
constructor(protected name: string, protected age: number) {
}
greeting(msg: string) {
console.log(`Hello! ${msg}`)
}
// 抽象メソッド
abstract profile(): void;
}
class Teacher extends Person {
constructor(name: string, age: number, private subject: string) {
super(name, age);
}
// 必ず実装する必要がある。
profile() {
//
}
}
abstract キーワードを付けたメソッドは抽象メソッドと呼ばれ、継承先のサブクラスにおいて実装しないとコンパイラがエラーを出すため、必ず実装する必要がある。
また、抽象クラスはインスタンス化できないため、必ず継承して使用することになる。
シングルトンパターンの実装
クラスから生成できるインスタンスを1つだけに制限するデザインパターンのことをシングルトンパターンと呼ぶ。
その実装方法の一つとして、constructor に private 修飾子を付ける方法がある。これにより、クラスの外では new 演算子によるインスタンス生成ができなくなることを利用する。
class Person {
private static instance: Person;
private constructor(protected name: string) {
}
greeting() {
console.log(`Hello! My name is ${this.name}.`);
}
static getInstance() {
if (Person.instance) return Person.instance;
Person.instance = new Person('Tom');
return Person.instance;
}
}
// const person = new Person('Tom', 38); // エラー
const person = Person.getInstance();
const person2 = Person.getInstance();
person.greeting(); // Hello! My name is Tom.
console.log(person === person2); // true
オブジェクトの型を定義する(interface)
インターフェース(interface)とは
「type」キーワードを使用すれば、オブジェクトの型にエイリアス(別名)を与えることができる。
type User = {
name: string;
age: number;
}
const user: User = {
name: 'Tom',
age: 32
}
一方で「interface」というキーワードを使用してもオブジェクトの型を表現することができる。
interface User {
name: string;
age: number;
}
const user: User = {
name: 'Tom',
age: 32
}
interface はオブジェクトの型のみを定義することができ、したがって、interface というキーワードが出てきたら、それはオブジェクトの型を定義しているのだと、見てすぐにわかるというメリットがある。
interface 同士でオブジェクトの型を継承する
interface は継承することもできるため、共通で利用するプロパティを抜き出して interface として定義することで再利用性を高めることができる。
interface Person {
name: string;
age: number;
}
interface User extends Person {
id: number;
}
const user: User = {
name: 'Tom',
age: 32,
id: 1000
}
interface で定義したオブジェクトの型を class に強制する(implements)
interface はオブジェクトの型を定義するものだった。このオブジェクトの型、つまりプロパティやメソッドを、新しく作成する class が持つこと(実装すること)を強制したい場合には、implements キーワードを使用する。
interface UserInterface {
name: string;
age: number;
greeting(msg: string): void;
}
class User implements UserInterface {
constructor(public name: string, public age: number) {}
greeting(msg: string) {
console.log(msg);
}
}
interface で定義されているプロパティやメソッドを class でも定義しないとコンパイラがエラーを出すため、プロパティやメソッドの実装が強制される。
プログラムの設計段階で、先に class が持つべきプロパティやメソッドを interface として定義しておき、class のコーディングの際には interface を継承することで実装のし忘れなどを防ぐことができるだろう。
また、interface は型の情報しか持たず、具体的な実装を含まないため、非常にシンプルに class の持つべき情報を表現できる。
inplements キーワードを使用して、class に interface を継承させるときには、以下のような特徴があるので注意されたい。
- interface は多重継承が可能。つまり複数の interface を1つの class に強制させることができる。
- interface で実装を強制できるプロパティやメソッドは public なものだけ。private や protected はダメ。
- static なプロパティやメソッドも強制できない。
- interface で定義した readonly 修飾されたプロパティについて、class 側でそのプロパティを持たなければいけないが、readonly は強制されない。
- class 側で interface には無いプロパティやメソッドを定義することはできる。
- extends による class の継承と implements による interface の継承は併用できる。
class User extends ParentClass implements UserInterface {…} と書く。
また、オブジェクトの型を interface ではなくて type エイリアスで定義しても、implements による class への強制は可能であることを一応述べておく。
プロパティを追加できるオブジェクトの型を定義する(インデックスシグネチャ)
interface などで定義したオブジェクトの型を適用すると、そのオブジェクトには型で定義されていないプロパティを追加することはできない。
プロパティが追加できるようなもっと制限の緩いオブジェクトを定義したい場合には、インデックスシグネチャと呼ばれる記述形式でプロパティを定義する必要がある。
インデックスシグネチャでは、オブジェクトのプロパティの型とその値の型の指定を以下のように記述する。
{ [index: string]: string; }
index の部分の名前は何でもよい。
type QueryParams = {
[param: string]: string;
}
const queryParams: QueryParams = {
q: 'search',
page: '3',
}
queryParams.count = '10';
interface QueryParams {
[param: string]: string;
}
const queryParams: QueryParams = {
q: 'search',
page: '3',
}
queryParams.count = '10';
在っても無くても良いオプショナルプロパティを持つオブジェクトの型を定義する
「?」記号をプロパティ名の後に付けることで、無くてもエラーにならないプロパティをもったオブジェクトの型を定義することができる。
const person: {
name: string;
age?: number;
} = {
name: 'Tom'
}
console.log(person); // { name: 'Tom' }
type Person = {
name: string;
age?: number;
}
const person: Person = {
name: 'Tom'
}
console.log(person); // { name: 'Tom' }
interface Person {
name: string;
age?: number;
}
const person: Person = {
name: 'Tom'
}
console.log(person); // { name: 'Tom' }
在っても無くても良いオプショナルパラメータ(引数)を持つ関数を定義する
関数の引数の後に「?」記号を付けることで、その引数が与えられなくてもエラーにならないようにすることができる。
function add(n1: number, n2?: number): number {
if (!n2) {
n2 = 0;
}
return n1 + n2;
}
console.log(add(3)); // 3
TypeScript の構造的部分型
TypeScript では構造的部分型と呼ばれる考え方を採用しているため、ある変数にオブジェクトを代入できるかどうかは、変数の型が定めるプロパティやメソッドを、代入しようとするオブジェクトが持っているかどうかで決まる。
つまり変数の型が定めるプロパティやメソッドを、オブジェクトが全部持っていれさえすれば、それ以外のものを持っていても代入できる。継承関係などは考慮されない。
type Person = {
name: string;
age: number;
greeting(msg: string): void;
}
let person: Person;
const user = {
name: 'Tom',
age: 32,
id: 1000,
greeting(msg: string) {
console.log(msg);
},
hello() {
console.log('Hello!');
}
}
person = user; // 代入できる
型A の全てのプロパティかつ型B の全てのプロパティを持つ intersection 型(交差型)
型A の全てのプロパティを持ち、かつ、型B の全てのプロパティを持つ。このような型を交差型(intersection 型)と呼ぶ。
次に示すように、intersection 型を作成するには、記号「&」を使用する方法と、interface の多重継承を使用する方法がある。
記号 & による intersection 型
次のように、記号「&」を使って intersection 型を作成することができる。
type Engineer = {
name: string;
role: string;
}
type Blogger = {
name: string;
follower: number;
}
// intersection 型
type EngineerBlogger = Engineer & Blogger;
const person : EngineerBlogger = {
name: 'Tom',
role: 'front-end',
follower: 1000
}
type エイリアスではなくて、interface を使用しても同様に定義することができる。
interface Engineer {
name: string;
role: string;
}
interface Blogger {
name: string;
follower: number;
}
// intersection 型
type EngineerBlogger = Engineer & Blogger;
const person : EngineerBlogger = {
name: 'Tom',
role: 'front-end',
follower: 1000
}
interface の多重継承による intersection 型
複数の interface を多重に継承することによっても intersection 型を作成することができる。
interface Engineer {
name: string;
role: string;
}
interface Blogger {
name: string;
follower: number;
}
// intersection 型
// type EngineerBlogger = Engineer & Blogger;
interface EngineerBlogger extends Engineer, Blogger {}
const person : EngineerBlogger = {
name: 'Tom',
role: 'front-end',
follower: 1000
}
intersection 型の補足
両立できない型A, B の場合、A & B は never 型になる
ただし、次のように両立できない型をしようすると、never 型になる。
// never 型になる
type NumberString = number & string;
条件文を使って型を絞り込む(Type guard)
Type guard は主に次の3つの演算子を用いて実現する。
- typeof 演算子
- in 演算子
- instanceof 演算子
Type guard による型の絞り込みは、主に Union 型(共用型)における型の絞り込みで使用する。
また、Union 型を構成するそれぞれの型にリテラル型のプロパティ(タグと呼ばれる)を持たせることで(タグ付き Union と呼ばれる)、タグで型を絞り込めるようにする記述パターンが知られている。
typeof 演算子による型の絞り込み
例えば、次のサンプルでは、number | string 型の引数 x が関数内部でどちらの型であるのかを絞り込むために、typeof 演算子を使用している。
function double(x: number | string): number | string {
if (typeof x === "number") {
return x * 2;
}
return (Number(x) * 2).toString();
}
const result2 = double(3.14);
console.log(result2); // 6.28
console.log(typeof result2); // number
const result1 = double('3.14');
console.log(result1); // 6.28
console.log(typeof result1); // string
in 演算子による型の絞り込み
例えば、次のサンプルでは、Engineer | Blogger 型の引数 nomadWorker が関数内部でどちらの型であるのかを絞り込むために、他方には含まれないプロパティが存在するかどうかを in 演算子で確認することで、型を絞り込んでいる。
type Engineer = {
name: string;
role: string;
}
type Blogger = {
name: string;
follower: number;
}
type NomadWorker = Engineer | Blogger;
function displayProfile(nomadWorker: NomadWorker) {
if ('role' in nomadWorker) {
// この中では Engineer と見なされる。
console.log(nomadWorker.role);
}
if ('follower' in nomadWorker) {
// この中では Blogger と見なされる。
console.log(nomadWorker.follower);
}
}
instanceof 演算子による型の絞り込み
例えば、次のサンプルでは、Dog | Bird 型の引数 pet が関数内部でどちらの型であるのかを絞り込むために、pet がそれらクラスのインスタンスであるかどうかを instanceof 演算子を使用して確認することで、型を絞り込んでいる。
class Dog {
speak() {
console.log('bow-wow');
}
}
class Bird {
speak() {
console.log('tweet-tweet');
}
fly() {
console.log('flutter');
}
}
type Pet = Dog | Bird;
function havePet(pet: Pet) {
pet.speak();
if (pet instanceof Bird) {
// この中では Bird クラスのインスタンスであると見なされる。
pet.fly();
}
}
havePet(new Dog());
// bow-wow
havePet(new Bird());
// tweet-tweet
// flutter
タグ付き Union を使用することで型を絞り込めるようにする
Union 型を構成するそれぞれの型にリテラル型のプロパティ(タグと呼ばれる)を持たせることで、タグで型を絞り込めるようにする記述方法がある。(参考:判別可能なUnion型)
class Dog {
kind: 'dog' = 'dog'; // タグを付ける
speak() {
console.log('bow-wow');
}
}
class Bird {
kind: 'bird' = 'bird'; // タグを付ける
speak() {
console.log('tweet-tweet');
}
fly() {
console.log('flutter');
}
}
type Pet = Dog | Bird;
function havePet(pet: Pet) {
pet.speak();
switch (pet.kind) {
case 'bird':
// この中では Bird クラスのインスタンスであると見なされる。
pet.fly();
}
}
havePet(new Dog());
// bow-wow
havePet(new Bird());
// tweet-tweet
// flutter
このサンプルは class の Union だったが、interface の Union でも同様にタグ付き Union を定義することができる。
interface Engineer {
kind: 'engineer';
name: string;
role: string;
}
interface Blogger {
kind: 'blogger';
name: string;
follower: number;
}
type NomadWorker = Engineer | Blogger;
function displayProfile(nomadWorker: NomadWorker) {
if (nomadWorker.kind === 'engineer') {
// この中では Engineer と見なされる。
console.log(nomadWorker.role);
}
if (nomadWorker.kind === 'blogger') {
// この中では Blogger と見なされる。
console.log(nomadWorker.follower);
}
}
型を強制的に上書きする(型アサーション)
型アサーションとは
TypeScrip によって推論された型や、既に定義してある変数の型を強制的に上書きすることができる(型アサーションと呼ぶ)。(参考:Type Assertion)
TypeScrip は必ずしも適切な推論をするとは限らないため、そのような場合には型アサーションを使って型を手動で上書きする必要がある。
型アサーションの書き方は、
- タグ < > を使う方法
- as を使う方法
の2通りが存在するが、タグを使う方法は JSX との相性が悪いため、as を使った書き方が推奨されている。
例:変数の初期値を空のオブジェクトにしたい場合
変数を空のオブジェクトで初期化しておいて、後からプロパティを追加したい場合もある。そのような場合は、変数の初期値である空のオブジェクトを class や interface により型アサーションする。このようにすることで、コンパイラがエラーを出すことを回避することができる。
interface QueryParams {
q: string;
page: number;
count: number;
}
// const queryParams = <QueryParams>{};
const queryParams = {} as QueryParams;
queryParams.q = 'Hello';
queryParams.page = 3;
あるいは次のようにプロパティにアクセスするときに変数を型アサーションしてもエラーを回避できる。
interface QueryParams {
q: string;
page: number;
count: number;
}
const queryParams = {};
(queryParams as QueryParams).q = 'Hello';
(queryParams as QueryParams).page = 3;
例:取得した HTML 要素を適切な型に変更したい場合
document.getElementById メソッドで取得した要素は、それが <input> 要素なのか <p> 要素なのかなどの要素の種類に関係なく、常に TypeScript によって HTMLElement | null 型と推論される。
HTMLElement 型は interface として定義されているが、全ての要素のプロパティを含んでいるわけではない。例えば、次のように <input> 要素の value プロパティにアクセスしようとするとエラーになる。
const input = document.getElementById('username');
if (input) {
console.log(input.value); // Property 'value' does not exist on type 'HTMLElement'.
}
したがって、要素によってはより適切な型に変更したい場合があり、そのようなときに型アサーションを使用する。
// const input = <HTMLInputElement>document.getElementById('username');
const input = document.getElementById('username') as HTMLInputElement;
if (input) {
console.log(input.value);
}
null や undefined ではないことをコンパイラに伝える(! 演算子)
「!」記号で表される演算子(Non-Null Assertion Operator)を使用することで、コンパイラに式の値が null や undefined ではないことを伝えることができる。
const h1 = document.getElementById('title');
console.log(h1!.innerText);
プロパティの参照がエラーにならないようにする(?. 演算子)
JavaScript では、undefined や null である変数からプロパティを参照しようとすると実行時にエラーが発生する。
TypeScript では、undefined や null になる可能性のある変数からプロパティを参照しようとすると、コンパイラがエラーを吐く。
このエラーを防ぐためには、プロパティを参照しようとしている変数が、undefined や null ではないことを確認してからプロパティにアクセスすればいいのだが、それを容易にしてくれる演算子としてオプショナルチェイニング演算子(Optional chaining)という「?.」で表される演算子が存在する。
この演算子を使ってプロパティを参照すると、null や undefined からプロパティを参照しようとすると undefined が返却される。
interface ResponseData {
userId: number;
userInfo?: {
name?: {
first: string;
last: string;
};
avatar: string;
}
}
const responseData: ResponseData = {
userId: 1
}
console.log(responseData.userInfo?.name); // undefined
console.log(responseData.userInfo?.name?.first); // undefined
ただし、このオプショナルチェイニング演算子は、TypeScript 3.7.0 以降で使用できることに注意が必要。
null や undefined のときに初期値を返す(?? 演算子)
式を評価したときに、null や undefined である場合には特定の値を返したい場合がある。そのような目的のための演算子として、Null 合体演算子(Nullish coalescing operator)という記号「??」で表される演算子がある。
OR 演算子「||」の場合は、左辺が falsy なときに右辺の値を返すが、Null 合体演算子「??」の場合は、左辺が null または undefined のときにのみ右辺の値を返す。
console.log(false || 'Hello'); // Hello
console.log(0 || 'Hello'); // Hello
console.log('' || 'Hello'); // Hello
console.log(null || 'Hello'); // Hello
console.log(undefined || 'Hello'); // Hello
console.log(false ?? 'Hello'); // false
console.log(0 ?? 'Hello'); // 0
console.log('' ?? 'Hello'); // ''
console.log(null ?? 'Hello'); // Hello
console.log(undefined ?? 'Hello'); // Hello
ただし、この Null 合体演算子は、TypeScript 3.7.0 以降で使用できることに注意が必要。
引数や戻り値が異なる同名の関数を定義する(オーバーロード)
次のような関数を考える。
function double(x: number | string) {
if (typeof x === "number") {
return x * 2;
}
return (Number(x) * 2).toString();
}
const result = double('100');
console.log(result.toUpperCase());
// error: Property 'toUpperCase' does not exist on type 'string | number'.
この例では 変数 result の値は ‘200’ という文字列になるため、文字列のメソッドである toUpperCase をコールしても問題ないように思えるが、コンパイラはエラーを出す。
これは関数 double の戻り値が string | number 型と推論されることが原因だ。
この関数は
- 引数 x が number なら戻り値も number
- 引数 x が string なら戻り値も string
であることを意図して作成してある。それであるなら、そもそも関数の定義を分けて同名の関数として
function double(x: number) {
return x * 2;
}
function double(x: string) {
return (Number(x) * 2).toString();
}
のように定義したい。他の言語なら引数の型が異なれば、別の関数に同じ名前を付けられる(オーバーロード)という仕組みがある。TypeScript にもオーバーロードの仕組みがある。しかし上の例のように関数の定義の実体を分けることはできず、シグネチャと呼ばれる関数の雛形を宣言することで、以下のようにして同名の関数を定義する。
function double(x: number): number; // シグネチャ
function double(x: string): string; // シグネチャ
function double(x: number | string) { // 関数の定義
if (typeof x === "number") {
return x * 2;
}
return (Number(x) * 2).toString();
}
const result = double('100');
console.log(result.toUpperCase()); // 200
つまりオーバーロードの手順は次のようになっている。
- シグネチャと呼ばれる定義したい関数の雛形を宣言する。
- シグネチャの直後に、具体的な実装を記述する。
この実装では、オーバーロードする全ての関数の引数の型を受け取れるようにして、処理の中で typeof 演算子や instanceof 演算子などを使用して型を判定して分岐させる。
シグネチャでは、関数の処理ブロック { } は不要で、セミコロン ; だけを記述する。
型を取得する
変数の型を取得する(typeof 演算子)
type エイリアスによる型の定義の中で typeof 演算子を使用すると変数の型を取得することができる。
typeof 演算子を式の中で使用する場合とは異なることに注意すること。
let obj = {
name: 'Tom',
age: 32
}
console.log(typeof obj); // object
type objType = typeof obj;
// type objType = {
// name: string;
// age: number;
// }
オブジェクトのメンバーの型を取得する(LookUp 型)
オブジェクトのメンバーの型を取得したい場合、次のようにブラケット [ ] を使用することで実現できる。
interface UserData {
id: number;
name: {
first: string;
last: string;
}
}
type idType = UserData["id"]; // number
type nameType = UserData["name"]; // {first: string; last: string;}
type firstType = UserData["name"]["first"]; // string
type idNameType = UserData["id" | "name"]; // number | {first: string; last: string;}
オブジェクトのキーの Union 型を取得する(keyof)
keyof キーワードを使用すると、オブジェクトのキーの Union 型を取得することができる。(参考:TypeScript 2.1)
interface UserData {
id: number;
name: {
first: string;
last: string;
}
}
type UserDataKey = keyof UserData; // "id" | "name"
型の互換性について
interface を使った関数の型の表現
interface を使って関数の型を定義する
type fnType = (w: number, h: number) => number;
関数の型はこの様に定義することができることは既に述べたが、同様のことを interface を使用しても定義することができる。
interface fnType {
(w: number, h: number): number;
}
オブジェクトのメソッド名を省略したような書き方になっている。
オーバーロードで定義した関数の型
例えば、関数を次のようにオーバーロードで定義する。
function double(x: number): number; // シグネチャ
function double(x: string): string; // シグネチャ
function double(x: number | string) { // 関数の定義
if (typeof x === "number") {
return x * 2;
}
return (Number(x) * 2).toString();
}
この関数 double は次のように型推論される。
したがって、オーバーロードで定義された関数の型を扱う必要があるなら、interface を使って(type エイリアスでもいい)、次のようにする。
interface doubleType {
(x: number): number;
(x: string): string;
}