TypeScript を使ってみる(基本型や型注釈)
TypeScirpt の型注釈(型付け)や基本的な型の使い方についての解説のような覚書です。
以下は JavaScript の基礎的な知識があることを前提にしています。
作成日:2023年11月12日
関連ページ
- TypeScript 環境の構築
- TypeScript 関数
- TypeScript クラス
- TypeScript 高度な型
- TypeScript モジュール
- TypeScript 非同期処理 Promise と Fetch
TypeScript とは
TypeScript は Microsoft によって開発されたオープンソース言語で、JavaScript に「静的型付け」を追加した JavaScript のスーパーセット(JavaScript の文法を拡張した上方互換言語)です。
そのため、TypeScript の用途は JavaScript と同じで、JavaScript の使える場面では代わりに TypeScript を使うことができます。
TypeScript の詳しい情報は以下のサイトなどで確認できます。
TypeScript の公式サイト
- https://www.typescriptlang.org/
- https://www.typescriptlang.org/ja/(日本語一部)
- https://github.com/microsoft/TypeScript
参考にさせていただいたサイト
- サバイバルTypeScript
- TypeScript Deep Dive 日本語版
- Recursion TypeScript入門
- @IT TypeScriptのTypeあれこれシリーズ
- TypeScriptの型入門
TypeScript のインストール
TypeScript のコードはそのままではブラウザや Node.js で実行できないので、TypeScript のパッケージをインストールして JavaScript にコンパイルする必要があります。
以下は Node.js の npm を使って TypeScript のパッケージをローカルインストールする手順です。
TypeScript のパッケージをインストールすると TypeScript コンパイラ(tsc
)もインストールされます。
ローカルインストールとは、特定のディレクトリにパッケージをインストールして、そのディレクトリだけでインストールしたモジュールを使う方法です。
※ 以下は、すでに Node.js(及び npm)がインストールされていることが前提です。また、エディタは TypeScript の開発元と同じ Microsoft によって開発されている VS Code を使用しています。
任意の名前のディレクトリを作成(以下の例では tsp という名前のディレクトリを作成)し、ターミナルを開いて作成したディレクトリに移動して以下を実行します。
npm init -y
で package.json を生成npm i -D typescript @types/node
で TypeScript コンパイラ(tsc
)と Node.js の型定義ファイル(Node.js の標準モジュールなどを利用できるようにするため)をローカルインストールnpx tsc --init
で tsconfig.json(コンパイラの設定ファイル)を生成
% npm init -y return //package.json を生成 //package.json の保存先と内容が表示される Wrote to /Applications/MAMP/htdocs/sample/package.json: { "name": "tsp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } % npm i -D typescript @types/node return //typescript と @types/node をインストール //2つのパッケージが追加される added 2 packages, and audited 3 packages in 1s found 0 vulnerabilities % npx tsc --init return //tsconfig.json を生成 //以下の内容の tsconfig.json が生成される Created a new tsconfig.json with: target: es2016 module: commonjs strict: true esModuleInterop: true skipLibCheck: true forceConsistentCasingInFileNames: true
これで TypeScript コンパイラ(tsc)がインストールされたので、作成したディレクトリの中に TypeScript ファイルを作成してコンパイルすることができます。
デフォルトでは、TypeScript ファイルをコンパイルすると同じ場所に同じ名前の JavaScript ファイルが生成されます。
この例では、src というディレクトリを作成して、その中に TypeScript ファイルを配置して、dist というディレクトリに出力するようにします。
作成したディレクトリ(この例の場合は tsp)の直下に src というディレクトリを配置して、index.ts という空の TypeScript ファイル(ファイル名は任意)をその中に作成します。
tsp ├── node_modules │ ├── @types │ └── typescript ├── package-lock.json ├── package.json ├── src //追加 │ └── index.ts //追加 └── tsconfig.json
コンパイラのオプションを設定するファイル tsconfig.json を開いて、例えば以下のように変更します。
以下の場合、include オプションの設定により src ディレクトリ内の全ての TypeScript ファイルがコンパイルの対象になり、outDir オプションの設定により dist ディレクトリにコンパイルされた JavaScript ファイルが出力されます。
{ "compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "node", "outDir": "./dist", //コンパイルされた JavaScript ファイルの出力先 "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "include": ["./src/**/*.ts"] //コンパイルの対象ファイル }
src ディレクトリに配置した TypeScript ファイル index.ts を以下のような内容にします。
const message: string = "hello, world!"; console.log(message);
この例では TypeScript パッケージをローカルインストールしているので、コンパイルするにはターミナルで npx を使って tsc
コマンドを実行します。
tsc にファイル名を指定しないで実行すると、tsconfig.json に設定した内容に従ってコンパイルが実行されます(エラーがあればその内容が表示されますが、成功すれば何も表示されません)。
% npx tsc return //コンパイルを実行
上記を実行すると、dist ディレクトリが作成され、その中に以下の JavaScript ファイル index.js が出力されます。
"use strict"; const message = "hello, world!"; console.log(message);
tsp ├── dist // dist ディレクトリが作成される │ └── index.js // コンパイルされて出力された JavaScript ファイル ├── node_modules │ ├── @types │ └── typescript ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json
node コマンドに上記の生成された JavaScript ファイルを指定して実行すると、ターミナルに「hello, world!」と出力されます。
% node dist/index.js return // node コマンドにファイルを指定して実行 hello, world!
ブラウザ(HTML)で表示
TypeScript のコードはブラウザで直接実行できないので、コンパイルした JavaScript ファイルを HTML ファイルで読み込みます。
tsp ├── dist │ └── index.js // HTML ファイルで読み込む JavaScript ├── index.html // HTML ファイルを追加 ├── node_modules │ ├── @types │ └── typescript ├── package-lock.json ├── package.json ├── src │ └── index.ts └── tsconfig.json
HTML ファイルでは、script タグの src 属性にコンパイルされて生成される index.js を指定して読み込みます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TypeScript Sample</title> </head> <body> <h1>TypeScript Sample</h1> </body> <!-- コンパイルされた JavaScript を読み込む --> <script src="dist/index.js"></script> </html>
ブラウザの開発者ツールのコンソールタブでコンソールへの出力を確認できます(ブラウザで表示)。
npm scripts
必要に応じて package.json の scripts フィールドにコマンドを指定します(npm run 〜
で指定したコマンドを実行することができます)。
{ "name": "tsp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "watch": "tsc --watch" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@types/node": "^18.13.0", "typescript": "^4.9.5" } }
上記の例では "watch": "tsc --watch"
を設定したので、npm run watch を実行すると以下のような表示になり、TypeScript ファイルが更新されると自動的にコンパイルが実行されます。
% npm run watch return //ファイルが更新されると自動的にコンパイル
watch モードを終了するには control + c を押します。
環境構築の詳細は TypeScript の開発環境 を御覧ください。
VS Code NPM スクリプトパネル
VS Code を使用していれば、左下に表示される NPM スクリプトパネルでクリックするだけで登録してある npm script を実行することができます。
静的型付け
プログラミング言語は動的型付け言語と静的型付け言語の2つに分類することができます。
動的型付け言語には PHP や JavaScript などがあり、静的型付け言語には C 言語や Java、TypeScript などがあります。
動的型付け言語と静的型付け言語では以下のような違いがあります。
- 動的型付け言語:「実行時」に型が決定される
- 静的型付け言語:「コンパイル時(実行時よりも前)」に型が決定される
動的型付け言語の場合、変数の型は実行時(ランタイム)に決定されるので、プログラムを実行するまで型の問題によるエラーは見過ごされてしまいます。
静的型付け言語の場合は、実行時よりも前のコンパイル時で型が決定されるので、開発中に早い段階で型の問題によるエラーに気づくことができます。また、VS Code などの TypeScript をサポートしているエディタでは、コードを書いている時点で(コンパイルする前に)エラーに気づくことができます。
TypeScript は型チェックの仕組みを持ったプログラミング言語(静的型付け言語)で、JavaScript のスーパーセット(上位互換)です。
型推論(Type inference)
TypeScript の特徴の1つに型推論があり、TypeScript コンパイラはソースコードを解析して変数や関数などの型を推論する(自動で判別する)ことができます。
以下の場合、変数 foo の宣言で値に文字列 'abc' が代入されているので、TypeScript は変数が string 型であると推論します。
そして、2行目では string 型の変数に number 型の値が代入されているのでエラーになります。
let foo = 'abc'; foo = 123; // 型推論により foo は文字列と推測されるので数値を代入するとエラーになる
VS Code の場合、変数にマウスオーバーすると型が表示されます。以下は foo にマウスを当てて推論された型を表示しています。また、以下のように2行目ではエラーが表示されます。
コンパイルを実行すると、ターミナルには以下のようなエラーが表示されます。
tsp % npx tsc //コンパイルを実行
src/index.ts:3:1 - error TS2322: Type 'number' is not assignable to type 'string'.
3 foo = 123;
~~~
Found 1 error in src/index.ts:3
型推論により、TypeScript では明示的に型を指定しなくても変数の型が存在します。但し、推論される型が適切であるとは限りません。
型チェックでエラーがあっても JavaScript は出力される
デフォルトの設定では、コンパイルエラーがあっても、TypeScript は有効な JavaScript を出力します。
例えば、先述の例のコンパイルエラーが発生する以下のコードをコンパイルすると
let foo = 'abc'; foo = 123; // コンパイルエラー
以下の JavaScript が出力されます。
"use strict"; let foo = 'abc'; foo = 123;
型チェックでエラーがある場合は JavaScript を出力しない
tsconfig.json で noEmitOnError
オプションに true
を指定することで、エラーがある場合は、JavaScript ソースコードなどを出力させないようにすることができます。
または、コンパイルが成功した時のみファイルを生成するコマンドラインオプション --noEmitOnError
を指定してコンパイルを実行すれば、エラーがある場合は JavaScript を出力しません。
型推論と動的型付けの違い
型推論と動的型付けは、型を指定しないという点は同じですが、以下のような違いがあります。
型推論はコンパイルのタイミングでコンパイラにより自動で型が決定され、以降はその型が変更されることはありません。
以下の場合、変数 foo が型推論により string 型として決定され、以降変数 foo は常に string 型として扱われます。そのため、2行目では string 型の変数に number 型の値を代入しているのでエラーになり、3行目では toExponential() メソッドは number のメソッドなのでエラーになります。
let foo = 'abc'; //foo は型推論により string 型 foo = 123; //string 型に number 型を代入しているのでエラー console.log(foo.toExponential()); //string 型に toExponential() メソッドはないのでエラー
VS Code では以下のように表示されます。
動的型付けでは実行時に型が決まるので、実行タイミングにより型が動的に変化します。
以下の JavaScript の場合、変数 foo に 'abc' が代入された時点での型は string 型となりますが、123 の数値を代入することで number 型に変更されます。
このように実行タイミングで型が動的に変化するため、TypeScript の型推論(静的型付け言語)ではエラーになる処理も動的型付け言語の JavaScript では正常に動作します。
let foo = 'abc'; //foo の型は string 型 foo = 123; //foo の型は number 型 console.log(foo.toExponential()); //toExponential() は数値型のメソッドなので正常に動作する
JavaScript では上記のコードでもエラーにはなりません。
明示的な型指定
型推論の結果が正しくなかったり望ましくない場合は、明示的に型を指定することができます。
型の指定のことを型注釈や型アノテーション(type annotation)と言います。
明示的に型を指定することによって、以下のメリットがあります。
- 型を指定することで、そのプログラムを読む人が理解しやすくなる
- コンパイラの型チェックのアルゴリズムにコードに対する正しい理解を反映できる
以下は変数 foo に string 型を指定する変数宣言の型注釈の例です。
const foo: string = 'abc';
型を指定した変数に型が一致しない値を代入すると、コンパイラはエラーを出力します。VS Code を使っていれば、エディター上にエラーが表示されます。
実際には型推論による型は正確ではなかったり正しくない可能性があるため、基本的には明示的に型を指定する(型注釈を付ける)ことになります。
例えば、以下のような関数を記述すると、関数の引数の型が指定されていないため TypeScript は正しい型を推論できません。
const subtract = (x,y) => { return x - y; } console.log(subtract(7,2));
正しい型を推測できないので、引数の型は any
となってしまっていて望ましい結果は得られません。
そのため、例えば以下のように引数や戻り値に明示的に型を指定する必要があります。
const subtract = (x: number,y: number): number => { return x - y; } console.log(subtract(7,2));
VS Code を使用している場合は、以下のように問題箇所にマウスオーバーして表示されるウィンドウの「クイックフィックス」をクリックすると
クイックフィックスの候補が表示されるので、いずれかをクリックすると
例えば、以下のように引数に型注釈が追加されます。
既存の JavaScript ライブラリでの型の利用
ほとんど全ての有名な JavaScript ライブラリの型定義は、DefinitelyTyped コミュニティによって既に作成されていて、名前が @types/ から始まるパッケージをインストールするだけで、既存の JavaScript ライブラリが簡単に利用できるようになっています(@types/ から始まるパッケージには型定義が記述された型定義ファイルが入っています)。
例えば、Node.js の標準モジュールを利用するには @types/node
(パッケージ)をインストールします。
% npm install --save-dev @types/node //@types/node をインストール
ライブラリなどの型定義は、アンビエント宣言と呼ばれる型宣言(declare
キーワードを使った変数の型情報の宣言)で行います。
アンビエント宣言は、型定義ファイル(拡張子は .d.ts)と呼ばれる別ファイルで管理することが推奨されています。型定義ファイルが用意されていない場合など、必要であれば自分で作成することもできます。
参考
- サバイバルTypeScript
- TypeScript Deep Dive 日本語版
型注釈(type annotation)
TypeScript では変数や関数、オブジェクトのプロパティなどを宣言する際に、型注釈(type annotation)を記述して明示的に型を指定することができます。
type は日本語で「型」、annotation は「注釈」なので type annotation は「型注釈」や「型アノテーション」などと呼ばれています。型を指定することを型注釈を書くなどとも言います。
変数宣言の型注釈(型アノテーション)
変数を宣言する際にその変数の型を明記して、どのような値(型)が代入可能かを指定できます。
以下が変数宣言の構文です。: 型
の部分が型注釈で、その変数の型を指定します。
const 変数 : 型 = 式;
以下は変数 greeting と to に対して string という型を指定しています。: string
が型注釈です。
これにより、コンパイラはこれらの変数は string 型であると認識し、異なる型の値を代入するとコンパイルエラーになります。
const greeting: string = "Hello, "; const to: string = "world!"; console.log(greeting + to);
例えば、変数 to に数値 123 を指定すると「to は string 型」という型注釈に反しているので型エラーになります。
const greeting: string = "Hello, "; const to: string = 123; //型注釈に反しているので型エラー console.log(greeting + to);
VS Code を使用している場合、変数 to に数値を指定した時点でエラーになり、エラー箇所を示す赤い下線が表示されている to にマウスオーバーすると以下のように表示されます。
または、左下の X のアイコンをクリックすると、「問題」タブにエラーが表示されます。
コンパイルすると以下のようなコンパイルエラーになります。
TypeScript では、式は常に何らかの型を持ちます。そして式の持つ型が代入先の型と一致しない場合は型エラーが発生します。
式(Expression)
値を生成し、変数に代入できるものは式になります。
7
のような数値や "hello"
のような文字列などのリテラル値、name
のような変数名や関数名などの識別子、1+2
のような式と演算子の組み合わせなどを式と呼びます。
式には結果があり、式を評価すると結果の値を得ることができます。以下に詳しい解説があります。
プリミティブ型
TypeScript における値は大きく分けると、プリミティブとオブジェクトに分けられます。
オブジェクトはプリミティブを組み合わせてできていて、複数(または1つ)の値で構成されますが、プリミティブはそれ以上の構成要素に分解できない単一の値です。
プリミティブ型は TypeScript で利用できる最も基本的なデータ型(他の型から派生していない基本的な値を表すデータ型)で、現時点では以下の7種類があります。
型 | 型注釈 | 説明 |
---|---|---|
数値型 | number | 1 や 0.2 などの数値(整数または浮動小数点数) |
文字列型 | string | "Hello" や 'hello' などの文字列 |
論理型 | boolean | true または false の真偽値 |
null 型 | null | 値がない(存在しない)ことを表す型。値は null のみ |
undefined 型 | undefined | 値が未定義であることを表す型。値は undefined のみ |
シンボル型 | symbol | 動的に一意で不変な値として生成される値(ES6 で導入) |
BigInt 型 | bigint | 桁数制限のない大きな整数型(ES2020 で導入)。例 1325786n |
TypeScript においては上記のプリミティブ型以外はオブジェクト型になります。
数値型
変数が数値であるという型注釈には number
を使います。
number 型では、整数と小数の区別がありません。
数値リテラル
コードの中に数値を書くと、それは数値リテラルとなります(リテラルとは何らかの値を生み出すための式のことです)。値を10進数でそのまま書く以外にも以下のような数値リテラルがあります。
2進数、8進数、16進数のリテラルでは小数点を使用することはできません。
リテラル | 説明 | 例 |
---|---|---|
2進数 | 先頭に 0b を付けて2進数を表します |
0b1011 (10進数 の 11) |
8進数 | 先頭に 0o を付けて8進数を表します |
0o732 (10進数 の 474) |
16進数 | 先頭に 0x を付けて16進数を表します |
0xeff (10進数 の 3839) |
指数表記 | 整数または小数の後に e と整数を付けて指数を表します |
3.7e8 (370000000) |
また、数値リテラルでは数字の間に _
を挟むことが許されているので、1000000
を 1_000_000
と記述したり、0xffffff
を 0xfff_fff
と記述することもできます。
NaN
NaN
は数値型の特殊な値で、数値計算が有効な数値で表現できない場合に返されます。
const foo = Number('foo'); //Number 関数に文字列を渡す console.log(foo); //NaN console.log(foo + 100);; //NaN を含む計算の結果は NaN
また、比較演算子や等価演算子を数値に対して使用する場合、いずれかのオペランドが NaN
の場合は常に false
が返されるので注意が必要です。NaN === NaN
も false
になります。
console.log(NaN === NaN); // false console.log(Number.isNaN(NaN)); // true
NaN
かどうかを判定するには Number.isNaN
を使用します。
const foo = Number('foo'); if(Number.isNaN(foo)) { // foo は数値ではないので以下が出力される console.log('foo は数値ではありません'); } /* // 以下の条件判定は必ず false を返すのでコンパイルエラーになります。 if(foo === NaN) { // 以下はは決して実行されない console.log('foo は数値ではありません') } */
BigInt 型
BigInt は ES2020 で追加された桁数制限のない大きな整数型です。
bigint 型を使用するには、tsconfig.json
のコンパイラーオプションで target
を es2020
以上にする必要があります。
変数が BigInt であるという型注釈には bigint
を使います。
BigInt を扱うには BigInt リテラル(123n
のように n
を整数の後に付ける)を使用します。
const bigx: bigint = 123n;
BigInt も number 型同様、四則演算などの計算をすることができ、結果も BigInt になります。
但し、BigInt は整数のみしか扱えないので除算の結果が少数になる場合、整数に丸められます。
const result = 7n / 3n; console.log(result); //2n
bigint 型と number 型の演算
bigint 型と number 型はそのままでは一緒に演算をすることはできません。
以下の場合、「error TS2365: Operator '+' cannot be applied to types '7n' and '5'.」のようなエラーになります。
//そのままでは一緒に演算できない const result = 7n + 5; //エラーになる
どちらかに型を合わせる必要があります。BigInt 関数を使うと bigint 型の数値を作成できます。また、Number 関数を使えば bigint 型の数値を number 型に変換できます。
const result: bigint = 7n + BigInt(5); console.log(result); //12n const result2: number = Number(7n / 2n) + 5; console.log(result2); //8
BigInt() には整数しか渡せない
BigInt() に小数を渡すとコンパイルはされますがランタイムエラーになり、例外が発生して強制終了します。
console.log(BigInt(5)); //5n console.log(BigInt(2.5)); //エラー(例外発生)
BigInt には NaN に相当する値はない
BigInt には NaN に相当する値がないため、数値を表していない値を BigInt() に渡すと小数を渡すのと同様、コンパイルは成功しますがランタイムエラーになります(例外が発生して強制終了します)。
const bigint = BigInt('foo'); //エラー(強制終了) console.log(bigint);
強制終了を避けるには、try〜catch 文で例外を捕捉します。
try { const bigint = BigInt("foo"); console.log(bigint); } catch (e) { console.warn("エラー発生 : " + e); //エラー発生 : SyntaxError: Cannot convert foo to a BigInt }
文字列型
変数が文字列であるという型注釈には string
を使います。
文字列リテラルにはダブルクォートを使う書き方 "Hello"
とシングルクォートを使う書き方 'Hello'
の2種類があります(機能上の違いはありません)。
また、文字列をバッククォート`
で囲むテンプレートリテラルというリテラルもあります。
テンプレートリテラルは、改行と式の挿入ができます。
普通の文字列リテラルの場合、リテラル中での改行は構文エラーになるので \n
などの改行シーケンスを入れる必要がありますが、テンプレートリテラルの中で実際に改行をすれば、そのとおりに反映されます。
const str: string = `Hello World!`; console.log(str); //改行されて出力される
テンプレートリテラルの中で ${式}
という構文を使って式を挿入することができます。
const str1: string = "hello"; const str2: string = 'world'; console.log(`${str1}, ${str2.toUpperCase()}`); //hello, WORLD
論理型(真偽値)
論理型(真偽値)は、true
と false
の2種類だけの値からなるプリミティブで、boolean 型と呼ばれます。真偽値を表現するための論理型(真偽値)リテラルは true
と false
です。
変数が論理型(真偽値)であるという型注釈には boolean
を使います。
const yes: boolean = true; const no: boolean = false; console.log(yes, no); // true false
真偽値への変換
真偽値は、true
と false
の2種類だけなので、どのような値も真偽値に変換するとどちらかになります。
console.log(Boolean(0)); // false console.log(Boolean(1)); // true console.log(Boolean(NaN)); // false console.log(Boolean(0n)); // false console.log(Boolean(1n)); // true console.log(Boolean("")); // false console.log(Boolean("foo")); // true console.log(Boolean(null)); // false console.log(Boolean(undefined)); // false const bar = {} // 空のオブジェクト console.log(Boolean(bar)); // true
型 | 変換結果 |
---|---|
数値 | 0 と NaN は false になり、その他は true |
BigInt | 0n は false になり、その他は true |
文字列 | 空文字列は false になり、その他は true |
null | false |
undefined | false |
オブジェクト | 全て true |
null と undefined
JavaScript 及び TypeScript では「値がない」に相当する表現に null と undefined の2つがあります。
null
と undefined
は、それぞれ「データがない」ことを表現するプリミティブで、それぞれ null
、undefined
という値と型をもちます。
言い換えると、null
型の値は null
という1種類だけで、undefined
型の値は undefined
という1種類だけです。
プログラムの中では、それぞれ null
や undefined
と書くことで利用できます。
型注釈も、それぞれ null
と undefined
です。
const nu: null = null; const un: undefined = undefined; console.log(nu, un); //null undefined
undefined と null の違い
undefined は「値が代入されていないため、値がない(初期化されていない)」、null は「代入すべき値が存在しないため、値がない(現在利用できない)」というような微妙な違いがあります。
変数を宣言したときに初期値がない場合や、オブジェクトに存在しないプロパティや配列にない要素にアクセスしたとき、戻り値がない関数の戻り値を取得したときなどでは undefined
になります。
JavaScript では、以下はいずれも undefined
を返します。
let value; //初期値がない(初期化されていない) console.log(value); //undefined const obj = {}; //オブジェクトに存在しないプロパティにアクセス console.log(obj.foo); //undefined const arr = []; //配列にない要素にアクセス console.log(arr[0]); //undefined function func() {} //戻り値がない関数の戻り値を取得 console.log(func()); //undefined
※ 一部の DOM 系の API(getElementById メソッドなど)は null
を返します。
const elem = document.querySelector('hello'); // hello というセレクタの要素を取得(そのようなセレクタは存在しない) console.log(elem); // null
x == null(undefined または null )
一致判定を行う等価演算子には == と === がありますが、== は異なる型の比較を行った場合、暗黙の型変換を行って比較するので、通常は厳格な一致判定をする === を使います。
但し、x == null という判定は例外で、「x が undefined または null である」という判定としてよく使われます。以下は引数の値が undefined または null の場合とそうでない場合で処理を分岐する例です。
function someFunc(x: unknown) { if(x == null) { // x が undefined または null の場合 }else{ // そうでない場合 } } // 引数の型注釈の unknown 型は型が何かわからないときに使います(※)。
※ someFunc() では引数に nullと undefined を含めて任意の型を取れるように引数の型を unknown 型としています。
x == null
は x === null || x === undefined
と同じことですが、短く記述することができます。
function someFunc(x: unknown) { if(x === null || x === undefined) { // x が undefined または null の場合(上記と同じこと) }else{ // そうでない場合 } }
null と undefined を == で判定すると true になり、=== で判定すると false になります。
console.log(null == undefined); // true console.log(null === undefined); // false
関連項目:オプショナルチェイニング | Null 合体演算子
参考
オブジェクト型
TypeScript ではプリミティブ型以外はオブジェクト型になり、オブジェクト型はオブジェクトを表す型です。オブジェクト型を使うことで、オブジェクトの型を宣言及び制御することができます。
オブジェクトの型注釈
VS Code で以下のオブジェクトリテラルを記述して、変数 obj にマウスオーバーすると、変数 obj の型がポップアップ表示されます。
const obj = { foo: 'abc', bar: 123 }
この例では型注釈を記述していませんが、型推論により変数の型は常に存在します。
上記 VS Code の表示(型推論の結果)によれば変数 obj の型は以下のように、『string 型の foo プロパティと number 型の bar プロパティを持つオブジェクト』の型ということになります。
const obj: { foo: string; //string 型の foo プロパティ bar: number; //number 型の bar プロパティ }
オブジェクト型の構文
オブジェクト型の構文(インラインの型注釈)は上記のように、プロパティ名: 型;
という宣言を {}
の中に記述します。
以下の型注釈の場合、宣言された型のプロパティ(プロパティ1、プロパティ2、プロパティ3)を全て持つオブジェクトという意味になります。
//オブジェクトの型の定義(インラインの型注釈) const オブジェクト名 : { プロパティ1: 型; プロパティ2: 型; プロパティ3: 型; }
オブジェクトの型注釈(型アノテーション)
オブジェクトの型注釈もプリミティブ型と同様、変数の宣言につけることができます。
//オブジェクトの型注釈 const オブジェクト名 : { プロパティ名: 型; プロパティ名: 型; ・・・ } = { プロパティ名: プロパティの値(式), プロパティ名: プロパティの値(式), ・・・ }
以下は前述の変数 obj の宣言に型注釈を記述した例です。=
の左側がインラインの型注釈(オブジェクト型の構文)になります。
const obj: { foo: string; bar: number; } = { foo: 'abc', bar: 123 } //以下のように1行で記述することもできます。 const obj: { foo: string; bar: number;} = { foo: 'abc', bar: 123 }
以下のように let
を使って型注釈とオブジェクトリテラルを分けて記述することもできます。
//型注釈 let obj: { foo: string; bar: number; }; //オブジェクトリテラル obj = { foo: 'abc', bar: 123 }
また、プロパティ名の部分はオブジェクトリテラルと同様、文字列リテラル(''
や ""
で囲んだ名前)や算出プロパティ名([式]
)を使うことができます。
const key1 = 'foo 123'; const key2 = 'bar 777'; const obj: { [key1]: string; [key2]: number; } = { [key1]: 'abc', [key2]: 123 } console.log(obj['foo 123']); //abc console.log(obj['bar 777']); //123
オブジェクト型の型チェック
オブジェクトに型注釈を記述することで、TypeScript に型チェックをしてもらえます。
初期化時や代入時に指定した型に合わないオブジェクトを変数に入れるとコンパイルエラーになります。
以下の場合、bar プロパティは number 型が宣言されているのに文字列が入っているのでコンパイルするとエラーになります。
const obj: { foo: string; //foo は string 型 bar: number; //bar は number 型 } = { foo: 'abc', bar: 'efg' //型注釈に反しているので型エラーになる }
VS Code を使用している場合、プロパティ bar に文字列を指定した時点で(コンパイル前に)エラーになり、エラー箇所を示す赤い下線が表示されている bar にマウスオーバーすると以下のように表示されます。
コンパイルを実行すると以下のようにエラーになります。
但し、型チェックでエラーがあっても JavaScript は出力されます。
型チェックでエラーがある場合は JavaScript を出力しない
型で宣言されているプロパティを持っていないオブジェクトを代入したり、宣言されていないプロパティを持つオブジェクトを代入した場合もコンパイルエラーになります。
const obj: { foo: string; bar: number; } = { foo: 'abc', // 型で宣言されているプロパティ(bar)を持っていないのでコンパイルエラーになる }
const obj: { foo: string; bar: number; } = { foo: 'abc', bar: 123, baz: true // 型で宣言されていないプロパティがあるのでコンパイルエラーになる }
型で宣言されていないプロパティにアクセスしてもコンパイルエラーになります。
const obj: { foo: string; bar: number; } = { foo: 'abc', bar: 123, } // 以下は型で宣言されていないプロパティにアクセスしているのでコンパイルエラーになる console.log(obj.baz);
type 文(型エイリアス)
type キーワードを使って型の名前(型エイリアス)を宣言して、型の別名を定義をすることができます。type 文は TypeScript 独自の文で JavaScript にはありません。
型エイリアスは type 型名 = 型;
で定義することができ、これにより指定された型名が新しく作成され、他の組み込みの型と同じように型注釈などで使えます。
type を使った型の別名(型エイリアス)は、プリミティブやオブジェクトなどどのような型にも付けることができます。型名は通常、大文字で始めます(キャメルケース)。
以下は文字列型または数値型(ユニオン型)の StringOrNumber という新しい型(型エイリアス)を作成する例です。
type StringOrNumber = string | number; // 型エイリアスを定義 // 作成した型名を使って型注釈を付ける let foo: StringOrNumber = "Foo!"; foo = 123; foo = false; // 文字列型または数値型でないのでエラー
type 文で作成(定義)した型エイリアスは、それまでになかったデータ型を作るわけではありません。
例えば、上記の例で定義した StringOrNumber は、string | number に名前(型の別名)を付けて、再利用しやすいようにしている(型を表す変数に代入している)だけです。
以下は type 文を使ったオブジェクトの型定義の構文です。オブジェクトリテラルと似ていますが、プロパティ名のコロンに続いてデータ型を記述し、末尾をセミコロンにします。
type 型名 = { プロパティ名: データ型; ... };
以下はオブジェクト型に型エイリアス(別名)を作成する例です。
// FooBar という型名(型の別名)を作成 type FooBar = { foo: string; bar: number; }; // 変数 obj の型注釈に FooBar を指定 const obj : FooBar = { foo: 'abc', // foo は string 型以外はエラーになる bar: 123, // bar は number 型以外はエラーになる };
このように型エイリアスを使ってオブジェクトの型を一度定義しておくと、オブジェクトリテラルを定義する際に、その型としてオブジェクトの型定義を指定できます(型注釈に使えます)。
また、type 文(型エイリアス)による型名の定義は、その型名を使用する後でもかまいません(型チェックはコンパイル時に行われるため、実際のプログラムの実行とは関係がないため)。
// FooBar を定義する前に使うこともできる const obj : FooBar = { foo: 'abc', bar: 123, }; // FooBar という型名を定義 type FooBar = { foo: string; bar: number; };
以下は、string 型の name と number 型の age プロパティと戻り値のない greet メソッドを持つオブジェクトの型 Person の定義の例です(関連:関数の型、メソッドシグネチャ)。
type Person = { name: string; age: number; greet: () => void; // または greet(): void; } const foo: Person = { name: 'Foo', age: 23, greet:function() { console.log(`Hello, my name is ${this.name}`); } } foo.greet(); // Hello, my name is Foo
インタフェース(interface)
型定義には前述の type 文(型エイリアス)以外にもインタフェースという仕組みも利用できます。 TypeScript のインターフェースはデータ構造を定義するために使われます。
インタフェースはオブジェクトやクラスが持つべきプロパティやメソッドを指定することができ、実装の詳細は提供されません。また、インタフェースは TypeScript 独自の仕組みで JavaScript にはありません。
インタフェースは interface キーワードを使ってオブジェクトの型名(インタフェース名)を宣言して、オブジェクトの型注釈として使用することができます。
type 文では任意の型に別名を宣言できますが、interface で扱えるのはオブジェクト型のみです。
以下が interface を使った宣言の構文です。この場合、型名はインタフェース名とも呼びます。
interface 型名 { プロパティ名: データ型; ... }
型エイリアスと違い、インタフェースは定義そのものなので、=
による代入式ではありません。
interface キーワードに型名(インタフェース名)を続けて、interface 型名 { オブジェクト型 }
のようにブロックにオブジェクトの型定義を記述し、代入式ではないのでブロックの後にセミコロンは不要です。
以下は前述の type 文での型エイリアスを interface を使って書き換えた例です。
type 文とほぼ同じですが、interface の場合は型名の後に =
がありません。
// FooBar という interface(型名)を定義 interface FooBar { foo: string; bar: number; } // 変数 obj の型注釈に FooBar を指定 const obj : FooBar = { foo: 'abc', bar: 123, } // Person というインターフェースを定義 interface Person { name: string; age: number; greet: () => void; // または greet(): void; } // 変数 foo の型注釈に Person を指定 const foo: Person = { name: 'Foo', age: 23, greet:function() { console.log(`Hello, my name is ${this.name}`); } } foo.greet(); // Hello, my name is Foo
interface と type との違い
インタフェースはオブジェクトの型指定に利用するためのものなので、既存の型をもとに型定義するような場合は利用できません。例えば、以下はエラーになります。このような場合は型エイリアスを使います。
interface StringOrNumber {
string | number // コンパイルエラー
}
殆どの場合、インタフェースは型エイリアス(type 文)で代用できますが、インタフェースにはできて型エイリアスにはできないものに、プロパティの自動追加機能(宣言マージ)があります。
宣言マージ
他の言語の interface 構文は、同じ名前のインターフェースを宣言するとエラーになるものが多いですが、TypeScript では同じ名前のインターフェースを宣言してもエラーにはなりません(この仕様のことを、オープンエンドと呼びます)。
TypeScript では、同じ名前のインターフェースを宣言した場合、それぞれのインターフェースの型がマージ(統合)されます。
例えば、以下のような同じ名前のインターフェースを宣言した場合、
interface Person { name: string; age: number; } interface Person { height: number; weight: number; }
インターフェースの名前が重なるからといってエラーとなるのではなく、プロパティが自動的にマージ(統合)され、結果的に Person インタフェースは、以下の定義と同じことになります。
interface Person { name: string; age: number; height: number; weight: number; }
この例の Person 型の場合、string 型の name、number 型の age、height、weight が必要です。
interface Person { name: string; age: number; } interface Person { height: number; weight: number; } // OK const foo: Person = { name: 'Foo', age: 18, height: 175, weight: 60 } // height、weight がないのでコンパイルエラー const bar: Person = { name: 'Bar', age: 20 } // Type '{ name: string; age: number; }' is missing the following properties from type 'Person': height, weight
型チェックはコンパイル時に行われるため、type 文や interface による型の定義は、その型名を使用する後で定義したものも適用されます。
そのため、以下の foo の定義はコンパイルエラーになります。
interface Person { name: string; age: number; } // 後から定義した heightと weight がないのでコンパイルエラー const foo: Person = { // Type '{ name: string; age: number; }' is missing the following properties from type 'Person': height, weight name: 'Foo', age: 18, } interface Person { height: number; weight: number; }
拡張
extends キーワードを使用すると、インターフェースを拡張することもできます。
既存のインターフェースのプロパティやメソッドをすべて継承した上で、新しいプロパティやメソッドを追加した新しいインターフェースを作成することができます。
interface Person { name: string; age: number; } const foo: Person = { name: 'Foo', age: 23 } // Person を拡張(プロパティを追加) interface PersonWithSize extends Person { height: number; weight: number; } // PersonWithSize は以下のプロパティを持つ const bar: PersonWithSize = { name: 'Bar', age: 27, height: 170, weight: 55 } // Person を拡張(メソッドを追加) interface PersonWithGreet extends Person { greet: ()=> void; // または greet(): void; } // PersonWithGreet は以下のプロパティとメソッドを持つ const baz: PersonWithGreet = { name: 'Baz', age: 18, greet: function() { console.log(`Hello, my name is ${this.name}.`) } }
型エイリアスは拡張は行えませんが、インターセクション型を使って似たようなことは実現可能です。
オプショナルなプロパティ
オブジェクト型ではプロパティ名の後ろに ?
を付けることでオプショナルな(任意のあってもなくてもよい)プロパティを宣言することができます。
以下は name プロパティとオプショナルな email プロパティを持つオブジェクトの宣言です。
user1 は email プロパティがあり、user2 は email プロパティがありませんが、いずれも User 型として認められ、コンパイルエラーになりません。
type User = { name: string; email?: string; // オプショナルなプロパティ }; const user1: User = { name: 'Foo', email: 'foo@example.com' }; const user2: User = { name: 'Bar' }; console.log(user1.email); // foo@example.com console.log(user2.email); // undefined
オプショナルなプロパティの型
VS Code で email の部分にカーソルを乗せると string | undefined
型と表示されますが、これはユニオン型と呼ばれる型で string 型または undefined 型を意味します。
オプショナルなプロパティの型は「指定した型」または undefined 型となります。
この例の場合、email プロパティにアクセスすると email プロパティが存在すれば string 型の値が返り、存在しなければ undefined が返ります(上記15、16行目)。
email?: string
は email?: string | undefined
と同じ意味として扱われるため、email プロパティ(オプショナルなプロパティ)に undefined を代入することもできます。
type User = { name: string email?: string; }; const user3: User = { name: 'Baz', email: undefined };
但し、email?: string
を ?
を付けずにemail: string | undefined
と書くのでは意味が異なります。
以下の場合、?
が付いていないので、email プロパティは必ず string または undefinned の値を指定しなければならず、省略するとコンパイルエラーになります。
こちらの方法の場合、値がない場合にプロパティを明示的に undefined に指定する必要があるので、書き忘れによるミスを防ぐことができます(書き忘れた場合は以下のようにコンパイルエラーになります)。
type User = { name: string; email: string | undefined; }; // OK const user1: User = { name: 'Foo', email: 'foo@example.com' }; // コンパイルエラー const user2: User = { name: 'Bar' }; //Property 'email' is missing in type '{ name: string; }' but required in type 'User'. // OK const user3: User = { name: 'Baz', email: undefined };
exactOptionalPropertyTypes
オプショナルなプロパティは「省略可能」と「undefined も代入可能」という2つの意味がありますが、コンパイラオプションの exactOptionalPropertyTypes を有効にすると「省略可能」という意味だけになり、undefined を代入するとコンパイルエラーになります。
exactOptionalPropertyTypes を有効にして undefined も代入可能にするには、? を使わず明示的に baz: number | undefined と書くか、省略可能且つ undefined も代入可能にするには baz?: number | undefined と書きます。
ネストしたオプショナルなプロパティ
以下の User 型では、email と address プロパティ及びネストした city プロパティがオプショナルなプロパティとなっています。
address プロパティはオプショナルなプロパティとなっているので、指定しないでも許されますが、指定した場合はネストした zip プロパティはオプショナルではないので、必ず指定する必要があります。
そのため、以下の user3 ではコンパイルエラーになります。
type User = { name: string; email?: string; // オプショナルなプロパティ address? : { // オプショナルなプロパティ city?: string; // オプショナルなプロパティ zip: string; // address を指定した場合は必須 } }; // OK const user1: User = { name: 'Foo', email: 'foo@example.com' }; // OK const user2: User = { name: 'Bar', address: { zip: '10010' } }; // コンパイルエラー(zip プロパティが指定されていない) const user3: User = { name: 'Baz', address: { city: 'New York' } }; // Property 'zip' is missing in type '{ city: string; }' but required in type '{ city?: string | undefined; zip: string; }'.
string | undefined 型(ユニオン型)の値
オプショナルなプロパティの型(この例の場合 string | undefined
)は、undefined とのユニオン型になり、undefined になる可能性があることを表しています。
そのため、例えば以下のように string 型のプロパティを使用するとコンパイルエラーになります。
type User = { name: string; email?: string; }; const user1: User = { name: 'Foo', email: 'foo@example.com' }; console.log(user1.email); // foo@example.com console.log(user1.email.length); //コンパイルエラー // 'user1.email' is possibly 'undefined'.
エラーの内容は「'user1.email' は 'undefined' の可能性があります。」というものです。
この例の場合、email プロパティが指定されていないと undefined になります。
undefined には length プロパティはないので、length プロパティにアクセスすると実行時にエラーとなるため、TypeScript はその可能性を検知してこのようにコンパイルエラーにします。
undefined になるかもしれない値を扱うには、undefined の可能性を排除する必要があります。
例えば、以下のように値が undefined でないことを確認して length プロパティを使うようにすればコンパイルエラーを回避できます。
if(user1.email !== undefined) { // user1.email が undefined でなければ length を利用 console.log(user1.email.length); // 15 }
undefined は false と判定されるので、以下のように記述することもできます。
if(user1.email) { console.log(user1.email.length); // 15 }
関連項目:ユニオン型と絞り込み
オプショナルなメソッド
オプショナルな(あってもなくてもよい)メソッドも定義することができます。オプショナルなメソッドの場合は、メソッド名の後(引数のカッコの前)に ?
を付けます。
type User = { name: string; hello?(): void; //オプショナルなメソッド } const foo: User = { name: 'Foo', hello() { console.log('Hello!') } } // hello() はオプショナルなので、なくても OK const bar: User = { name: 'Bar' } // hello() は undefined の可能性があるので undefined でないことを確認して呼び出す if(foo.hello !== undefined) foo.hello(); // Hello! // または if(foo.hello) foo.hello();
オプショナルチェイニング
ES2020 で導入された JavaScript のオプショナルチェイニング演算子 (?.
) を使った構文を使うと、ネストしたプロパティの存在確認とアクセスを簡単に行うことができます。
通常のチェーン演算子(.
) を使った obj.prop
というプロパティアクセスの場合、obj が null や undefined であれば JavaScript では例外が発生し、TypeScript ではコンパイルエラーになります。
オプショナルチェイニングを使って obj?.prop
とすると、obj が null や undefined の場合でも例外やコンパイルエラーは発生せずに undefined が返されます。
但し、オプショナルチェイニングは、宣言済みの変数に対してのみ機能します(宣言されていないオブジェクトでは使用できませんが、未定義のオブジェクトでは使用できます)。
以下の User 型の address と city はオプショナルなプロパティで undefined の可能性があります。
この例の city のようなネストしたプロパティにアクセスする場合は、プロパティの存在を順番に確認してからアクセスする必要があるため、関数 getCity() はまず user.address が undefined でなく、そして user.address.city が undefined でない場合に user.address.city プロパティにアクセスしています。
type User = { id: number; name: string; address?: { // オプショナルなプロパティ city?: string; // オプショナルなプロパティ }; } // 引数に User 型のオブジェクトを受け取る関数 function getCity(user: User): string { // プロパティの存在を順番に確認してからアクセス if (user.address !== undefined && user.address.city !== undefined) { return user.address.city; } return 'Unknown'; } const foo: User = { id: 1, name: 'Foo', address: { city: 'New York' } }; const bar: User = { id: 2, name: 'Bar', }; console.log(getCity(foo)); // New York console.log(getCity(bar)); // Unknown
undefined は false と判定されるので、関数 getCity() は以下のように、もう少し短く記述できます。
function getCity(user: User): string { if (user.address && user.address.city) { return user.address.city; } return 'Unknown'; }
以下はオプショナルチェイニングを使って関数 getCity() を書き換えた例です。
オプショナルチェイニングを使って user.address?.city
とすることで、user.address にアクセスできない場合はそれ以上評価せずに undefined が返され、user.address にアクセスできるが user.address.city が未定義の場合も undefined が返されます。
そして user.address.city が定義されていてアクセスできれば、その値を返しています。
※ ?.
の前(左)にある変数やプロパティの値が null または undefined の場合、その先(右)のプロパティは評価されず、undefined が即座に返ります。
function getCity(user: User): string { if (user.address?.city) { return user.address.city; } return 'Unknown'; }
上記の関数 getCity() は以下のように更に簡潔に記述することもできます。
function getCity(user: User): string { return user.address?.city || 'Unknown'; }
または ||
の代わりに ??
(Null 合体演算子) を使ってもこの場合は同じですが、city の値が空文字列だった場合に結果が異なります(?? の場合は Unknown ではなく空文字列が返ります)。
function getCity(user: User): string { return user.address?.city ?? 'Unknown'; }
null や undefined の可能性があるオブジェクト
オプショナルチェイニングを使うと、アクセスするオブジェクトが null や undefined の場合でもランタイムエラー(例外)は発生せず、アクセスした結果は undefined になります。
以下の関数 printName() は引数の user が User 型のオブジェクトであれば、その name プロパティを取得して出力し、引数 user が undefined やnull の場合は undefined が出力されます。
オプショナルチェイニングを使って user?.name としているので、user が undefined や null の場合は undefined が返されるので user.name(undefined.name や null.name)へのアクセスは発生しないため例外は発生せず、変数 name には undefined が入ります。
type User = { name: string; }; // 引数には User 型のオブジェクトまたは undefined か null を受け取る関数 function printName(user: User | undefined | null) { // name の型は string | undefined const name = user?.name; // または const name = user?.['name']; console.log(name); } const foo: User = { name: 'Foo' } const undefinedUser = undefined; const nullUser = null; printName(foo); // Foo printName(undefinedUser); // undefined printName(nullUser); // undefined(null でも返されるのは undefined)
また、8行目のコメントのように、オプショナルチェイニング演算子はプロパティアクセスのブラケット記法と組み合わせて ?.["プロパティ名"]
のように記述することもできます。
配列のオプショナルチェイニング
配列要素を参照する際にもオプショナルチェーンが使えます。要素を参照する場合は、カギカッコの[]
の直前に ?.
を書きます。
以下の場合、arr は undefined なので arr[0]
とすると例外が発生してしまいますが、arr?.[0]
とすることで undefined が返されます。
const arr = undefined; const elem = arr?.[0]; // 例外は発生しない console.log(elem); // undefined
関数呼び出しのオプショナルチェイニング
関数を呼び出すときにもオプショナルチェイニングが使えます。関数呼び出しの場合は、引数のカッコ()
の直前に ?.
を書きます。
以下の関数 printStringFunc() は第1引数に StringFunc 型の関数または undefined を受け取ります。
引数に受け取る関数 strFunc は undefined の可能性があるので、strFunc を呼び出す際はオプショナルチェイニングを使ってstrFunc?.(str)
のように呼び出しています。
// 関数の型宣言 type StringFunc = (str: string) => string; function printStringFunc(strFunc: StringFunc | undefined, str: string) { // 関数呼び出しのオプショナルチェイニング(result は string | undefined) const result = strFunc?.(str); console.log(result); } const hello: StringFunc = function (str: string) { return `Hello, ${str}!`; }; let googbye = undefined; printStringFunc(hello, "Foo"); // Hello, Foo! printStringFunc(googbye, "Bar"); // undefined
オプショナルチェイニングを使うことで、呼び出す関数が null または undefined でない場合にのみ関数を呼び出すことができます。
上記の場合、引数に受け取った関数 strFunc が存在すれば result はその呼び出しの結果(戻り値)の string 型になり、strFunc が存在しない場合は undefined が返るので result は undefined となります。
関連項目:関数の型
メソッド呼び出しのオプショナルチェイニング
メソッド呼び出しのオプショナルチェイニングも、関数の場合と同様に、引数のカッコ()
の直前に ?.
を書いて呼び出せます。
以下の関数 checkObj() 内では obj?.myMethod?.()
という式を使用して、obj が存在し、myMethod が存在する場合に myMethod を実行しています。そしてその戻り値が true の場合は関数 printName を呼び出し、そうでなければメッセージを表示しています。
これにより、myMethod が実装されていないオブジェクトや null が渡されても安全に処理できます。
type Obj = { name: string; // オプショナルなメソッド myMethod?(): boolean; } // オブジェクトの name プロパティの値をコンソールに出力する関数 function printName(obj: Obj) { console.log(`Name is ${obj.name}.`); } // オプショナルチェイニングを使用して、myMethod が存在すれば myMethod を実行する関数 function checkObj(obj: Obj | null) { // obj が存在し、myMethod が存在すれば myMethod を実行してその戻り値を判定 if(obj?.myMethod?.()) { // myMethod の戻り値が true であれば printName を実行 printName(obj); }else{ // objやmyMethodが存在しない場合、myMethodの戻り値がfalseの場合はメッセージを表示 console.log('Not available.') } } // myMethod が実装されてるオブジェクト const foo: Obj = { name: 'Foo', myMethod(){ // name プロパティの長さが1より大きいかを返す return this.name.length > 1; } } // myMethod が実装されていないオブジェクト const bar: Obj = { name: 'Bar' } checkObj(foo); // Name is Foo. checkObj(bar); // Not available. checkObj(null); // Not available.
以下は、上記の例と似ていますが、Obj 型のオブジェクトは myMethod を持っています。
この例では checkObj 関数内で obj?.myMethod() という式を使用して、obj が存在する(null でない)場合に myMethod を実行しています。
これにより、checkObj() は null が渡されても安全に処理できます。
type Obj = { name: string; myMethod(): boolean; // 必須のメソッド }; function printName(obj: Obj) { console.log(`Name is ${obj.name}.`); } function checkObj(obj: Obj | null) { // obj が存在すれば myMethod を実行し、その戻り値が true であれば printName を実行 if(obj?.myMethod()) { printName(obj); }else{ console.log('Not available.') } } const foo: Obj = { name: 'Foo', myMethod(){ return true; } }; checkObj(foo); // Name is Foo. checkObj(null); // Not available.
Null 合体演算子
Null 合体演算子 (??
) は左辺が null または undefined の場合に右辺の値を返し、それ以外の場合は左辺の値を返します。
以下の関数 printUserName は name プロパティの値が undefined でなければその値を出力します。
空文字列は null でも undefined でもないため、24行目では空文字が出力されます。
type User = { id: number; name?: string; // オプショナルなプロパティ(string | undefined) } function printUserName(user: User) { // user.name が定義されていればその値を、undefined の場合は 'No name' を出力 console.log(user.name ?? 'No name'); } const user1: User = { id: 1, name: 'Foo' } const user2 :User = { id: 2, name: '' } const user3 :User = { id: 3 } printUserName(user1); // Foo printUserName(user2); // '' (空文字列) printUserName(user3); // No name
以下はオプショナルチェイニングと Null 合体演算子と組み合わせて使う例です。
以下の場合、関数 getUserName() の引数 user が null や undefined の場合はオプショナルチェイニングにより undefined となります。また、name プロパティが存在しない場合も undefined となります。
言い換えると、user?.name が undefined の場合、Null 合体演算子により'No name'が返されます。
type User = { id: number; name?: string; } function getUserName(user: User | null | undefined) { // オプショナルチェイニングと Null 合体演算子を組み合わせて使用 return user?.name ?? 'No name'; } const user1: User = { id: 1, name: 'Foo' } const user2 :User = { id: 1, name: '' // 空文字 } const user3 :User = { id: 3 } const user4 = null; const user5 = undefined; console.log(getUserName(user1)); // Foo console.log(getUserName(user2)); // '' (空文字列) console.log(getUserName(user3)); // No name console.log(getUserName(user4)); // No name console.log(getUserName(user5)); // No name
user?.name ?? 'No name'
を user?.name || 'No name'
とすると、空文字列は false と判定されるので26行目も No name となります。
関数 getUserName() の引数 user が null や undefined の場合を区別したい場合はオプショナルチェイニングを使わずに以下のように記述することができます。
type User = { id: number; name?: string; } function printUserName(user: User | null | undefined) { if(user) { return user.name ?? 'No name'; }else{ if(user === null) { return 'user は null'; } return 'user は undefined' } } const user1: User = { id: 1, name: 'Foo' } const user2 :User = { id: 1, name: '' // 空文字 } const user3 :User = { id: 3 } const user4 = null; const user5 = undefined; console.log(printUserName(user1)); // Foo console.log(printUserName(user2)); // '' (空文字列) console.log(printUserName(user3)); // No name console.log(printUserName(user4)); // user は null console.log(printUserName(user5)); // user は undefined
参考
読み取り専用プロパティ
オブジェクト型ではプロパティ名の前に readonly
キーワードを指定することで、そのプロパティを読み取り専用にすることができます。
読み取り専用に指定したプロパティは、再代入しようとするとコンパイルエラーになります。
type MyObj = { readonly foo: string; // 読み取り専用に指定 } const obj : MyObj = {foo: 'Foo'}; // 読み取り専用なので再代入しようとするとコンパイルエラーになる obj.foo = 'bar';
インデックスシグネチャ(インデックス型)
インデックスシグネチャは、オブジェクトのプロパティに対して任意のキーを許可するための機能です。
通常、オブジェクトは固定されたプロパティを持ちますが、オブジェクトのキー(プロパティ名)に対して特定の型を指定することで、動的なプロパティを持つオブジェクトを表現できます。
インデックスシグネチャは、オブジェクト型のプロパティの実際の名前は不明だけれど、それらが参照するデータの型はわかっている場合に便利です。
以下はオブジェクトのプロパティのキー(プロパティ名)を string 型、値を number 型にする例です。
type MyObj = { [key: string]: number; }; const obj: MyObj = { foo: 10, bar: 7, }; console.log(obj.foo); // 10 console.log(obj["bar"]); // 7
以下は上記を interface やオブジェクトリテラルで書き換えた例です
interface MyObj { [key: string]: number; }; const obj: MyObj = { foo: 10, bar: 7, };
const obj: { [key: string]: number; } = { foo: 10, bar: 7, };
インデックスシグネチャ(インデックス型)は [key: キーの型]: 値の型
のように記述します。
key
は型変数で任意の文字列を指定できますが、key や K にするのが一般的です。
プロパティ名は文字列で表されるので、キーの型は string、number のみが指定できます(TypeScript 4.4 からは symbol と template string pattern も指定可能になっています)。
インデックスシグネチャを使う場合、固定されたプロパティも持つことができます。
type MyObj = { fixedProp: number; // 固定されたプロパティ [key: string]: number; }; const obj: MyObj = { fixedProp: 80, foo: 100, }; console.log(obj.fixedProp); // 80 console.log(obj.foo); // 100
他のプロパティの値の型は同じでなければならない
上記のように、インデックスシグネチャを指定した型に他のプロパティを指定することはできますが、値の型は同じでなければなりません。
以下の場合、fixedProp はインデックスシグネチャで指定した値の型と異なるのでエラーになります。fixedProp の値の型とインデックスシグネチャの値の型を同じにする必要があります。
type MyObj = { fixedProp: string; // エラー(インデックスシグネチャの値の型と異なるため) [key: string]: number; };
以下のように階層が異なれば、値の型が異なっていても関係ありません。
type User = { name: string; id: number; // 異なる型(階層が異なるので問題ない) preferences: { [key: string]: string; } }; const currentUser: User = { name: 'Foo', id: 7, preferences: { lang: 'en', }, }; console.log(currentUser.preferences.lang);; // en
template string pattern
TypeScript 4.4からは template string pattern(テンプレート文字列のパターン)もインデックスシグネチャに利用できます。
type MyObj = { [key in `hello-${string}`]: string ; }; const obj: MyObj = { 'hello-foo': 'Hello Foo!', 'hello-bar': 'Hello Bar!', //'baz': 'Hello Baz!' //これはパターンにマッチしないのでコンパイルエラーになる }; console.log(obj['hello-foo']); // Hello Foo!
インデックスシグネチャは特定のパターンを持つオブジェクトを表現する際に便利ですが、あまり使いすぎると型の精度が下がる可能性があるため、適切に使う必要があります。
明確なプロパティで表現できる場合は、インデックスシグネチャを使わずに明示的なプロパティを定義する方が型の安全性が高くなります。
noUncheckedIndexedAccess オプション
以下では obj.bar というプロパティが存在しないにも関わらず、9行目でコンパイルエラーになりません。
obj.bar は実際には undefined ですが、TypeScript は obj.bar が number 型だと解釈するため、型安全性が失われています。
type MyObj = { [key: string]: number; }; const obj: MyObj = { foo: 10, }; const bar: number = obj.bar; //※デフォルトではコンパイルエラーにならない console.log(bar); // undefined
インデックスシグネチャがあるオブジェクト型では、実際にプロパティが存在するかどうかに関わらず、どんな名前のプロパティにもアクセスできてしまい、型安全性が失われてしまいます。
TypeScript 4.1以降では、noUncheckedIndexedAccess オプションを有効にすることで特定のキーに対応するプロパティが存在しない場合に、コンパイル時にエラーを出すことができます。
noUncheckedIndexedAccess を有効にした場合、インデックス型ではプロパティの型は自動的にプロパティに指定した型と undefined 型のユニオン型になります(プロパティが存在しないときに、値が undefined になるのを正確に型で表すため)。
このオプションを有効にするには設定ファイル tsconfig.json で noUncheckedIndexedAccess オプションを true に設定します。
{ "compilerOptions": { "noUncheckedIndexedAccess": true, // その他のコンパイラオプション } }
上記オプションを有効にすると先述のコードには以下のようなエラーが発生します。
noUncheckedIndexedAccess はインデックス型のプロパティや配列要素を参照したとき undefined のチェックを必須にする(undefined である可能性があるものとして扱う)コンパイラオプションです。
このオプションを有効にすると、配列はインデックス記法でアクセス(インデックスアクセス)をすると undefined 型とのユニオン型と解釈されるため、undefined のチェックが必要になります。
また、このオプションを有効した場合、配列 T[]
(T は型)から分割代入すると T 型もしくは undefined 型を示す T | undefined 型のユニオン型になります。
部分型
部分型(structural subtyping)は、2つの異なる型が互換性がある場合、片方の型がもう片方の型の部分型であるとみなされる仕組みです。
2つの型が部分型関係であるかどうかは、それらの型の構造が一致するかどうかに基づいて判定され、名前やインターフェースの宣言に依存しません。
例えば、次の2つのオブジェクト型は部分型関係があり、Employee 型は Person 型の部分型になります。
type Person = { name: string; age: number; } type Employee = { name: string; age: number; id: string; }
部分型であるかどうかは型の形状(プロパティやメソッドの構造)に基づいて判定されます。言い換えると、片方の型がもう片方の型の条件を満たしているかどうかで判定されます。
具体的には、型 S と型 T がオブジェクト型の場合、以下の2つの条件が満たされれば、型 S は型 T の部分型になります。つまり、型 S は型 T としての条件を満たしていると判定されます。
- 型 T が持つプロパティは全て型 S にも存在する
- 型 T の各プロパティと対応する型 S のプロパティの型は同じ型(またはその部分型)である
型 S が型 T の部分型であれば、型 S は型 T の条件を満たしているので、型 S の値は型 T の値でもあり、型 S は型 T としても扱うことができます(型 S のインスタンスを T の型として使用できることを意味します)。
先の例をもう少し詳しくみてみます。
Person 型の定義は「name プロパティが string 型で、かつ age プロパティが number 型であるオブジェクト」です。name と age 以外のプロパティは持っていても持っていなくても構いません。
Employee 型はこれらのプロパティを2つとも持っているので、Employee 型は Person 型の部分型であるということができ、Employee 型の変数やオブジェクトは、Person 型としても使うことができます。
type Person = { name: string; age: number; } type Employee = { name: string; age: number; id: string; }
例えば、以下の場合、employeeBob は Employee 型ですが、Person 型としても使用できます。
これは、部分型の概念により、Employee 型が Person 型の要件を満たすためです。
const personAlice: Person = { name: "Alice", age: 30, }; console.log(personAlice); //{name: 'Alice', age: 30} const employeeBob: Employee = { name: "Bob", age: 25, id: "12345", }; const personBob: Person = employeeBob; // OK console.log(personBob); //{name: 'Bob', age: 25, id: '12345'}
但し、Person 型のオブジェクトの name と age 以外にアクセスするとコンパイルエラーになります。
const employeeBob: Employee = {
name: "Bob",
age: 25,
id: "12345",
};
const personBob: Person = employeeBob;
console.log(personBob);
// {name: 'Bob', age: 25, id: '12345'}
console.log(employeeBob.id); // 12345
console.log(personBob.id); // コンパイルエラー
// error TS2339: プロパティ 'id' は型 'Person' に存在しません。
逆に以下のように、Person 型を Employee 型に代入しようとすると「プロパティ 'id' は型 'Person' にありませんが、型 'Employee' では必須です」というエラーになります。
これは Person 型が Employee 型の全てのプロパティを持っていない、つまり Person 型が Employee 型の要件を満たさないためです。
const personAlice: Person = {
name: "Alice",
age: 30,
};
const employeeAlice: Employee = personAlice; // コンパイルエラー
// error TS2741:プロパティ 'id' は型 'Person' にありませんが、型 'Employee' では必須です
オブジェクトリテラルでの余剰プロパティへのエラー
オブジェクトリテラルの場合は、オブジェクト型や interface とは異なるので注意が必要です。
部分型関係により、18行目のオブジェクト型を使った employeeBob の代入は問題ありませんが(先述の例と同じ)、20行目のオブジェクトリテラルでの代入はエラーになります。
type Person = { name: string; age: number; } type Employee = { name: string; age: number; id: string; } const employeeBob: Employee = { name: "Bob", age: 25, id: "12345", }; const personBob: Person = employeeBob; // OK const personJohn: Person = { name: "John", age: 23, id: "67890", // コンパイルエラー };
20行目からのオブジェクトリテラルの型は Person 型の部分型になっているので Person 型に代入できそうですが、以下のように「オブジェクトリテラルは既知のプロパティのみ指定できます」というエラーになります。
このようにオブジェクトリテラルの場合は厳密な型の一致が求められ、余計なプロパティを持つオブジェクトはエラーになります。これはプログラマーのミスを防止するため(?)のようです。
ジェネリック型(型引数を持つ型)
型引数を使うと特定の型を指定せずに、実際に利用されるまで型が確定しない、動的に決定できる汎用的な型を定義することができます。型引数を持つ型をジェネリック型と呼びます(※ジェネリック型はジェネリクスを意味する場合もあるようです)。
型引数は、ジェネリック型の定義内で使用される型のパラメータ(使用される型を指定するための変数のようなもの)で、実際の型と置き換えられるものです。
型を定義する際に、型引数は <
と>
で囲って指定します。型引数の名前は一般的に、T や U などが使われますが、1文字である必要はなく、任意の有効な識別子を名前として使用できます。
型引数は定義の中で型変数として使うことができます。
以下はオブジェクト型の定義の例です。
この例では User は2つの型引数 T と U を持ちます。定義の中の T と U(型変数)は型引数 <T, U> を参照します。
type User<T, U> = { name: string; birthday: T; id: U; }
上記の場合、User<T, U> 型は、name プロパティが string 型で、birthday プロパティは T 型、id プロパティは U 型であるオブジェクトの型という意味になります。
User<T, U> 型を使う側では、User<string, number> のように T と U に対応する型を指定します。
const foo: User<string, number> = { name: 'foo', birthday: '1/2/2001', // string id: 1 // number } const bar: User<Date, string> = { name: 'bar', birthday: new Date('2002-05-25'), // Date id: '0123' // string }
型引数を持つ型を type 文や interface を使って定義した場合、使う際には必ず <
>
で全ての型引数を指定する必要があります。
必要な型引数を指定せずに型を使用した場合はコンパイルエラーになります。
例えば、User 型を型引数の指定をせずに使おうとすると以下のようにコンパイルエラーになります。
型引数に制約をつける
type 文や interface で型引数を使う場合、単に型引数を列挙しただけではどんな型の引数も受け入れることになります。
extends キーワードを用いることで、何らかの特定の型を受け入れるように型引数に制約をつける(特定の型に限定する)ことができます。
具体的には、型引数の後に extends に続けて限定する型を指定することで、型引数を指定した型またはその部分型に限定できます。
以下では T extends UserId
として、型引数 T を UserId 型またはその部分型に限定しています。そのため、この例では制約を満たすための UserId 型を定義しています。
24行目の User<string> は制約を満たしていないのでコンパイルエラーになります。
// 型 T を特定の型(UserId 型またはその部分型)に限定
type User<T extends UserId> = {
name: string;
info: T;
}
// 制約用のオブジェクト型を定義
type UserId = {
id: string;
}
const fooId: UserId = {
id: '001'
}
const foo: User<UserId> = {
name: 'foo',
info: fooId
}
console.log(foo); // {name:'foo', info: {id: '001'}}
// 以下の <string> は制約を満たしていないのでエラーになります。
const foo2: User<string> = { // コンパイルエラー
name: 'foo2',
info: fooId
}
//24: error TS2344: 型 'string' は制約 'UserId' を満たしていません。
以下は、型引数に UserId 型の部分型である UserInfo 型を指定して使用する例です。
// 型 T を UserId 型またはその部分型に限定 type User<T extends UserId> = { name: string; info: T; } // 制約用のオブジェクト型 type UserId = { id: string; } // UserId の部分型 type UserInfo = { id: string; birthday: Date; } const barInfo: UserInfo = { id: '002', birthday: new Date('2002-05-025') } // 型引数に UserId 型の部分型を指定 const bar: User<UserInfo> = { name: 'bar', info: barInfo } console.log(bar); // {name: 'bar',info: {id: '002', birthday: Sat May 25 2002}}
以下は User 型から UserWithInfo 型を別途定義する例です。
// 型 T を UserId 型またはその部分型に限定 type User<T extends UserId> = { name: string; info: T; } // 制約用のオブジェクト型 type UserId = { id: string; } // UserId の部分型 type UserInfo = { id: string; birthday: Date; } // User 型から UserWithInfo を別途定義 type UserWithInfo = User<UserId>; // または User<UserInfo>; const bazInfo: UserInfo = { id: '003', birthday: new Date('2001-02-022') } const baz: UserWithInfo = { name: 'baz', info: bazInfo } console.log(baz); // {name: 'baz',info: {id: '003', birthday: Thu Feb 22 2001}}
型引数にデフォルト値を設定
型引数の宣言時に、型引数にデフォルト値を設定することができます。
型引数を持つ型を type 文や interface を使って定義した場合、使う際には必ず全ての型引数を指定する必要がありますが、デフォルト値を設定することで使用する際に型引数の指定を省略することができます。
デフォルト値を型引数に設定するには、宣言時に型引数の後に =
に続けてデフォルトの型を指定します。
以下は User 型の2つの型引数 T と U のどちらにもデフォルト値を設定する例です。
// 型引数にデフォルト値を設定 type User<T = UserName, U = UserId> = { name: T; info: U; } type UserName = { firstName: string; lastName: string; } type UserId = { id: string; } const uname: UserName = { firstName: 'John', lastName: 'Doe' } const uid: UserId = { id: '007' } // 型引数の指定を省略(User<UserName, UserId> と同じこと) const jd : User = { name: uname, info: uid } console.log(jd); // {name: {firstName:'John',lastName:'Doe'}, info:{id: '007'}} // User<string, UserId> と同じこと const jd2: User<string> = { name: 'John Doe', info: uid } console.log(jd2); // {name: 'John Doe', info: {id: '007'}} const jd3: User<string, string> = { name: 'John Doe', info: 'id: 007' } console.log(jd3); //{name: 'John Doe', info: 'id: 007'}
上記の34行目の User<string>
では型引数のうち1つのみを指定していますが、この場合、最初の型引数 UserName を string に指定して、UserId を省略したことになります。2番目(右側)の型引数のみを指定することはできません。
また、型引数にデフォルト値を設定する際は、全てに設定する必要はなく、設定しない型引数と混ぜて指定することもできます。
// 型引数 U にのみデフォルト値を設定 type User<T, U = UserId> = { name: T; info: U; } type UserName = { firstName: string; lastName: string; } type UserId = { id: string; } const uname: UserName = { firstName: 'John', lastName: 'Doe' } const uid: UserId = { id: '007' } // 型引数の指定を省略(User<UserName, UserId> と同じこと) const jd : User<UserName> = { name: uname, info: uid }
但し、デフォルト値を設定した型引数の後に、設定していない型引数を指定することはできません。その場合は、順番を入れ替えます。
// エラー
type User< T = UserName, U> = {
name: T;
info: U;
}
// OK
type User< U,T = UserName> = {
name: T;
info: U;
}
extends(制約)と同時に使う
extends を使って制約をつけた型引数にデフォルト値を設定することもできます。
// 型引数に制約とデフォルト値を同時に設定 type User<T extends UserName = UserName, U = UserId> = { name: T; info: U; }
配列型
TypeScript では配列の型もあり、配列の型注釈では配列の要素の型を指定します。
配列の型注釈は、T[]
と Array<T>
の2通りの書き方があります(T は型)。どちらを使っても意味は全く同じです。
T[]
変数名:
の後に配列の要素の型[]
を記述します。
以下は number 型、string 型、 number 型または string 型(ユニオン型)の値を要素に持つ配列の例です。
const numbers: number[] = [1, 2, 3, 4, 5]; const words: string[] = ['a', 'b', 'c']; const arr: (number | string)[] = [1, 'a'];
以下は type 文を使った例です。
type NumArray = number[]; const numbers: NumArray = [1, 2, 3, 4, 5];
Array<T>
変数名:
の後にArray<型>
を記述します。以下は前述の配列を書き換えた例です。
const numbers: Array<number> = [1, 2, 3, 4, 5]; const words: Array<string> = ['a', 'b', 'c']; const arr: Array<string | number> = [1, 'a'];
以下は type 文を使った例です。
type StrArray = Array<string>; const words: StrArray = ['a', 'b', 'c'];
以下はオブジェクト型の値を要素に持つ配列の型注釈を 型[]
で記述した例です。
const arr: { name: string; }[] = [ { name: 'foo'}, { name: 'bar'}, { name: 'baz'}, ];
Array<型>
を使って以下のように記述することもできます。
const arr: Array<{ name: string; }> = [ { name: 'foo'}, { name: 'bar'}, { name: 'baz'}, ];
type 文を使って以下のように記述することもできます。
type NameObjArray = { name: string; }[]; /* または type NameObjArray = Array<{ name: string; }> ; */ const arr: NameObjArray = [ { name: 'foo'}, { name: 'bar'}, { name: 'baz'}, ];
型推論
以下は型注釈を付けずに VS Code で変数名にマウスオーバーして、型推論による配列の型を表示して確認する例です。
配列の操作
以下の配列 words は型注釈 string[]
により string 型の値を持つ配列です。
そのため、string 型の値を追加したり、string 型の値で変更することはできますが、それ以外の型の値を追加したり、代入するとコンパイルエラーになります。
const words: string[] = ['a', 'b', 'c']; words[3] = 'd'; console.log(words); // ['a', 'b', 'c', 'd'] words[2] = 2; //コンパイルエラー
以下は配列の push() メソッドを使って配列の末尾に要素を追加する例です。
配列 numbers は型注釈 number[]
により number 型の値を持つ配列なので numbers.push() に渡せる値は number 型の値に限られます。
5行目の numbers.push('hello') は文字列を引数に指定しているのでコンパイルエラーになります。
const numbers: number[] = [1, 2, 3]; numbers.push(4); console.log(numbers); //[1, 2, 3, 4] numbers.push('hello'); // コンパイルエラー // 文字列型の引数を number 型のパラメーターに割り当てることはできないのでエラー
読み取り専用の配列
型注釈で配列を読み取り専用(readonly)とすることができます。
配列の型注釈同様、2通りの書き方があります(どちらを使っても意味は全く同じです)。
readonly T[]
配列の型注釈 T[]
(T は型)の前に readonly キーワードを指定すると読み取り専用の配列型にできます。
以下は readonly number[]
と指定して、その変数の型を number 型の読み取り専用配列にする例です。
const arr: readonly number[] = [1, 2, 3, 4, 5];
ReadonlyArray<T>
以下は前述の例を ReadonlyArray<T>
(T は型)で書き換えた例です。
const arr: ReadonlyArray<number> = [1, 2, 3, 4, 5];
読み取り専用配列型の配列を変更しようとするとコンパイルエラーが発生します。
const numbers: readonly number[] = [1, 2, 3, 4, 5]; numbers.push(6); // コンパイルエラー const words: ReadonlyArray<string | number> = [1, 'a', 3]; words.pop(); // コンパイルエラー
タプル型
タプル型(tuple type)は要素数が固定された配列で、配列の各要素に異なる型を指定することができる特徴があります。
タプル型は [string, number]
のように[]
の中に要素の型をカンマ区切りで並べて記述します。
例えば、[string, number]
は2要素のタプル型で、1番目(インデックスが0)の要素が string 型、2番目(インデックスが1)の要素が number 型になります。
以下は3要素のタプル型の例です。要素の位置に応じて型が決まっており、各要素の型が持っているプロパティ、メソッドを使用できます。
要素数を超える位置にアクセスするとコンパイルエラーになります。
const tuple1: [number, string, boolean] = [1, "ok", true]; console.log(++tuple1[0]); // 2 (number 型) console.log(tuple1[1].length); // 2 (string 型) console.log(tuple1[2].valueOf()); // true (boolean 型) tuple1[0] = 7; // OK tuple1[1] = 7; // コンパイルエラー(string 型に number 型は代入できない) //error TS2322: Type 'number' is not assignable to type 'string'. const foo = tuple1[3]; // コンパイルエラー(要素数を超える位置にアクセス) //error TS2493: Tuple type '[number, string, boolean]' of length '3' has no element at index '3'.
タプル型の注意点
タプル型は実際は配列なので配列のメソッドが使えます。
以下は2要素のタプル型で、[0] の要素が number 型、[1] の要素が string 型ですが、push() や pop() などのメソッドを使うと以下のような操作ができてしまい、コンパイルエラーは発生しません。
そのため、このような使い方には注意が必要です。
const tuple2: [number, string] = [100, 'foo']; tuple2.push(7); console.log(tuple2); // 3要素になる [100, 'foo', 7] tuple2.shift(); console.log(tuple2); // 型がずれる ['foo', 7] tuple2.pop(); console.log(tuple2); // 1要素になる ['foo'] const bar = tuple2[1]; // tuple2[1] は存在しない console.log(bar); // undefined console.log(tuple2[0]); // 現在 tuple2[0] は 'foo' になってしまっている tuple2[0] = 'bar'; // コンパイルエラー(型注釈では tuple2[0] は number 型) tuple2[0] = 1; // OK
ラベル付きタプル型
タプル型の各要素の前に 識別子:
としてラベルを付けることができます。
以下のタプル型は [0] の要素に name というラベルが、[1] の要素に id というラベルが付けられています。これによりタプル型の各要素が何を表しているかがわかりやすくなります。
但し、ラベルはタプル型の定義の中にしか存在しないため、foo.name のようにアクセスすることはできません。タプル型のラベルは定義の中でわかりやすくするという以上の意味や機能はありません。
type User = [name: string, id: number]; const foo: User = ['foo', 55]; console.log(foo[1]); // 55 console.log(foo.name); //コンパイルエラー
読み取り専用タプル型
タプル型も readonly
を指定することで要素を読み取り専用にすることができます。
const okStatus: readonly [string, number] = ['OK', 200]; okStatus[1] = 404; // 読み取り専用なので要素を変更するとコンパイルエラー // Cannot assign to '1' because it is a read-only property.
但し、変数そのものは上書きできます。
let nwStatus: readonly [string, number] = ['OK', 200]; nwStatus = ['Not Found', 404]; // OK
要素数と型が同じ場合、readonly なしのタプル型から readonly タプル型への代入はできます。
let foo: readonly [number,string] = [1, "foo"]; let bar: [number,string] = [2, "bar"]; foo = bar; // OK(readonly なしから readonly タプル型への代入) foo[0] = 3; // 読み取り専用なのでコンパイルエラー
その逆の readonly タプル型から readonly なしのタプル型への代入はできません。
let foo: readonly [number,string] = [1, "foo"]; let bar: [number,string] = [2, "bar"]; bar = foo; // コンパイルエラー // The type 'readonly [number, string]' is 'readonly' and cannot be assigned to the mutable type '[number, string]'.
以下の場合、readonly タプル型の値を変更可能(readonly なし)タプル型の引数に渡しているのでコンパイルエラーになります。
const func = (tuple:[number, number])=>{ console.log(tuple[0] * tuple[1]); }; const tupleValue: readonly [number,number] = [3, 5]; func(tupleValue); // コンパイルエラー // Argument of type 'readonly [number, number]' is not assignable to parameter of type '[number, number]'.
引数の型も readonly にすればコンパイルエラーになりません。
const func = (tuple: readonly [number, number])=>{ console.log(tuple[0] * tuple[1]); }; const tupleValue: readonly [number,number] = [3, 5]; func(tupleValue); // OK
オプショナルの要素を持つタプル型
タプル型の定義で型の後ろに ?
を付けることでオプショナルな(あってもなくてもよい)要素とすることができます。
以下は3つ目の要素をオプショナルとする例で、2要素の配列も3要素の配列もこのタプル型として扱うことができます。
この例の場合、3つ目の要素の型は number | undefined になります。関連:オプショナルなプロパティ
type OptionalTuple = [string, number, number?]; const foo: OptionalTuple = ['foo', 10, 100]; const bar: OptionalTuple = ['bar', 20]; // foo[2] は undefined になる可能性があるので値を扱うには undefined の可能性を除外 if(foo[2] !== undefined) { console.log(foo[2] * 10); }
オプショナルな要素を複数持つことも、全てをオプショナルにすることもできます。
但し、必須の要素をオプショナルな要素の後ろに配置することはできません。
type OptionalTuple1 = [string, number?, string?]; // OK type OptionalTuple2 = [string?, number?, string?]; // OK // 必須の要素をオプショナルな要素の後に置けない type OptionalTuple3 = [string, number?, string]; //コンパイルエラー // A required element cannot follow an optional element.
関連項目:可変長タプル型
分割代入
分割代入 (Destructuring assignment) 構文を使うと、配列の値やオブジェクトのプロパティを取り出して(分割して)、変数に代入することができます。
通常の代入と同じく代入演算子 =
を使いますが、左辺のオペランドがオブジェクトリテラルや配列リテラルとなります。
オブジェクト
分割代入を使うとオブジェクトの中から必要なプロパティを取り出して変数に代入することができます。
オブジェクトの場合、左辺は {}
で囲み、取り出すプロパティの名前(変数)をカンマ区切りで並べます。
代入演算子 =
で右辺のオブジェクトから同じ名前のプロパティの値を取り出して左辺の変数に代入します。
以下では、user オブジェクトの uname プロパティの値を変数 uname に、uid プロパティの値を変数 uid に代入しています。
const user = { uname: 'John Doe', age: 39, uid: '001', } //分割代入 {プロパティ名, ...} = オブジェクト; const {uname, uid} = user; console.log(uname); // John Doe console.log(uid); // 001
左辺のカンマ区切りの識別子は取得したいオブジェクトのプロパティ名を指定する役割と、取得したプロパティの値を格納する変数名の役割を併せ持っています。
リネーム(異なる名前で変数に取得)
プロパティ名と異なる名前の変数を使いたい場合は、プロパティ名
をプロパティ名: 変数名
とします。
const user = { uname: 'John Doe', age: 39, uid: '001', } // プロパティ名と異なる名前の変数にする場合 const {uname: userName, uid: userId} = user; console.log(userName); // John Doe console.log(userId); // 001
型推論
分割代入で宣言した変数の型は型推論によって決まります。
基本的に変数の型は、その変数に代入されるプロパティの型と同じになります。
以下の場合、uid は型推論により string 型になるので、数値(number 型)を代入しようとするとコンパイルエラーになります。
const user = {
uname: 'John Doe',
age: 39,
uid: '001',
}
// let で宣言して再代入を可能に
let {uname, uid} = user;
uid = 7; // コンパイルエラー(文字列型に数値型は代入できない)
// error TS2322: Type 'number' is not assignable to type 'string'.
また、存在しないプロパティを分割代入で取得しようとするとコンパイルエラーになります。
対応する値がない場合のデフォルト値を設定することができます。
const user = {
uname: 'John Doe',
age: 39,
uid: '001',
}
const { email } = user; // コンパイルエラー
// error TS2339: Property 'email' does not exist on type '{ uname: string; age: number; uid: string; }'.
型注釈
分割代入の左辺に型注釈を指定することができます。
const user = { uname: 'John Doe', age: 39, uid: '001', } // 左辺に型注釈を指定 let {uname, age}: {uname: string, age: number} = user; console.log(uname); // John Doe console.log(age); // 39
type 文や interface を使った型注釈も指定できます。
const user = { uname: 'John Doe', age: 39, uid: '001', } type User = { uname: string; age: number; uid: string } // type 文を使った型注釈を指定 const {uname, age, uid}: User = user;
ネストしたオブジェクト
ネストしたオブジェクトのプロパティを変数に取り出すには、オブジェクト名:{ プロパティ名 }
のようにオブジェクトの階層構造を指定します(階層の分だけ中カッコ{}
で囲みます)。
以下の場合、ネストした user オブジェクトの age プロパティは user: { age }
として変数 age に取得できます。トップの階層の id は、変数 id に obj オブジェクトの id プロパティの値が取得されます。
const obj = { id: 7, user: { name: 'Foo', age: 34 } } // ネストしたオブジェクトのプロパティの分割代入 const { id, user: { age } } = obj; /* 上記の分割代入は以下と同じこと const { id, user: { age } } = obj; */ console.log(id); // 7 console.log(age); // 34
デフォルト値(既定値)の指定
分割代入では対応する値がない(プロパティが存在しない)場合のデフォルト値を=
で指定できます。
分割代入で取得しようとしたプロパティが存在しない場合、対応する変数に undefined が入りますが、デフォルト値を設定することで undefined の代わりに設定した値を変数に代入することができます。
この機能は、オプショナルなプロパティを持つオブジェクトを分割代入する際に利用すると便利です。
以下の場合、オブジェクト user は email プロパティを持っていないので、デフォルト値を設定していなければ、変数 email には undefined が入りますが、デフォルト値を設定しているので 変数 email には 'no email' が入ります。
type User = { uname: string; email?: string; // オプショナルなプロパティ } // User 型のオブジェクト const user: User = { uname: 'John Doe', } // age にデフォルト値を設定 const { uname, email = 'no email' } = user; console.log(uname); // John Doe console.log(email); // no email
もし、以下のようにオプショナルなプロパティを設定していないオブジェクトに、単にデフォルト値を設定しただけの場合はコンパイルエラーになります。
const user = {
uname: 'John Doe',
}
const { uname, email = 'no email' } = user; // コンパイルエラー
// error TS2339: Property 'email' does not exist on type '{ uname: string; }'
上記のような使い方はあまりないと思いますが、分割代入の左辺にオプショナルプロパティを設定した型注釈を指定すればエラーを回避することができます。
const user = { uname: 'John Doe', } const { uname, email = 'no email' }: {uname: string; email?: string;} = user;
※また、デフォルト値は undefined にのみ適用されるので、値が null のときは null がそのまま代入され、デフォルト値は適用されません。
ネストしたオブジェクトのデフォルト値
ネストしたオブジェクトにデフォルト値を設定することもできます。
以下の場合、option プロパティはオプショナルのため、存在しない可能性があります。そのため19行目のように option: {nickname}
で分割代入しようとすると、存在しない(undefined の)値に対して分割代入はできないためエラーになります。
エラーを回避するには、option にデフォルト値を設定します。以下の場合、option プロパティが存在しない場合はデフォルト値として {nickname:'no nickname'}
を設定しています。
type User = { id: number; option?: { // オプショナル nickname: string; } } const user1: User = { id: 1 } const user2: User = { id: 2, option: { nickname: 'foo' } } //const {id, option: {nickname}} = user1; //コンパイルエラー const {id, option: {nickname} = {nickname:'no nickname'}} = user1; console.log(id, nickname); // 1 'no nickname' //異なる名前(id2 と nickname2)で変数に取得 const {id: id2, option:{nickname: nickname2} = {nickname: 'no nickname'}} = user2; console.log(id2, nickname2); // 2 'foo'
Rest パターン
Rest パターン...
を利用すると、分割代入で個々の変数に割り当てられなかった残りのプロパティを1つのオブジェクトにまとめることができます。Rest パターンは一番最後でのみ指定できます。
以下の場合、変数 rest には obj の bar プロパティと baz プロパティがコピーされたオブジェクトが、変数 rest2 には foo プロパティと baz プロパティがコピーされたオブジェクトが代入されます。
const obj = { foo: 'hello', bar: 123, baz: true } const {foo, ...rest} = obj; console.log(foo); // hello console.log(rest); // {bar: 123, baz: true} const {bar, ...rest2} = obj; console.log(bar); // 123 console.log(rest2); // {foo: 'hello', baz: true}
配列
配列の分割代入を使うと、配列の各要素を変数にそれぞれ代入することができます。
配列の場合、左辺は[]
で囲み変数をカンマ区切りで並べて記述し、配列の要素を先頭から順に取得します(変数名は任意の名前を指定できます)。
以下は配列 arr の最初の2要素を取り出して変数 val1 と val2 に代入しています。
const arr = [1, 2, 3]; // 配列の分割代入(最初の2要素を取り出して変数 val1 と val2 に代入) let [val1, val2] = arr; console.log(val1, val2); // 1 2
存在しない要素を分割代入で取得した場合は、変数に undefined が代入されます(コンパイルエラーにはなりません)。対応する値がない場合のデフォルト値を設定することができます。
const arr = [1, 2, 3]; // 存在しない要素に対して分割代 let [val1, val2, val3, val4] = arr; console.log(val1, val2, val3, val4); // 1 2 3 undefined
但し、右辺に配列を直接記述すると、右辺の配列リテラルは TypeScript ではタプル型とみなされ、以下のようなコンパイルエラーになります。
let [val1, val2, val3, val4] = [1, 2, 3];
// error TS2493: Tuple type '[number, number, number]' of length '3' has no element at index '3'.
型推論
分割代入で宣言した変数の型は型推論によって決まり、基本的に変数の型はその変数に代入される配列の要素の型になります。val1 と val2 は number[]型の配列から分割代入したので number 型になります。
const arr = [1, 2, 3];
let [val1, val2] = arr;
// val1 は number 型なので以下の代入はOK
const num: number = val1;
val1 = 5;
// val1 は number 型なので string 型は代入できない
val1 = 'a'; // コンパイルエラー
// error TS2322: Type 'string' is not assignable to type 'number'.
但し、tsconfig.json で noUncheckedIndexedAccess を有効にしている場合は、配列T[](T は型)から分割代入すると T 型もしくは undefined 型を示す T | undefined
型のユニオン型になります。
以下の場合、number[]型の配列から分割代入しているので、val1 と val2 の型は number | undefined
になります。そのため number 型の変数 num に直接代入するとコンパイルエラーになります。
const arr = [1, 2, 3]; let [val1, val2] = arr; // val1 は number | undefined 型なので以下はコンパイルエラー const num: number = val1; // error TS2322: Type 'number | undefined' is not assignable to type 'number'.
上記のコンパイルエラーは、以下のように val1 が number 型であること(または undefined ではないこと)を確認すれば回避できます。
const arr = [1, 2, 3]; let [val1, val2] = arr; // val1 が number 型であることを確認 if(typeof val1 === 'number'){ const num: number = val1; } // val1 が undefined ではないことを確認 if(val1 !== undefined) { const num: number = val1; } // この場合は Falsy ではない(Truthy である)ことを確認 してもOK if(val1) { const num: number = val1; }
または、以下のように number 型のデフォルト値を指定すると val1 は number 型になり、エラーを回避できます。
const arr = [1, 2, 3]; // デフォルト値を指定 let [val1 = 0, val2] = arr; const num: number = val1;
また、配列 arr を以下のようなタプル型にしても val1 は number 型になり、エラーになりません。
// タプル型を指定 const arr: [number, number, number] = [1, 2, 3]; let [val1, val2] = arr; const num: number = val1;
Compiler Options: noUncheckedIndexedAccess
配列の各要素が異なる型の場合、型推論は各要素の型と undefined のユニオン型になり、全て同じ型の要素の場合とは異なります。
以下の場合、変数の型は型推論により string | number | boolean | undefined
となり、8〜10行目の代入はコンパイルエラーになりません。
const arr = [1, 'hello', true]; let [num, str, bool] = arr; console.log(num, str, bool); // 1 'hello' true // 以下はコンパイルエラーが発生しない num = 'goodbye'; str = true; bool = 3; console.log(num, str, bool); // goodbye true 3
配列をタプル型として設定すると、各要素はタプル型で設定された型になるので、以下の再代入はコンパイルエラーになります。
// タプル型として設定 const arr: [number, string, boolean] = [1, 'hello', true]; let [num, str, bool] = arr; console.log(num, str, bool); // 1 'hello' true //以下の代入はコンパイルエラー num = 'goodbye'; // Type 'string' is not assignable to type 'number' str = true; // Type 'boolean' is not assignable to type 'string' bool = 3; // Type 'number' is not assignable to type 'boolean'
タプル型
先述の例のように配列の分割代入はタプル型に対しても使用できます。タプル型に対して分割代入を行った場合、それぞれの要素の型はタプル型の対応する要素の型となります。
const user: [string, number] = ['foo', 33]; const [uname, age] = user; console.log(uname, age); //foo 33
ネストした配列
ネストした配列の要素を変数に取り出すには、ネスト構造と一致するようにブラケット[]
を追加します。
const arr = [[1, 2], [3, 4, 5]]; const [[a, b], [c]] = arr; console.log(a, b, c); // 1 2 3
但し、noUncheckedIndexedAccess を有効にしていると、上記のコードは以下のようなコンパイルエラーになります。
const arr = [[1, 2], [3, 4, 5]];
const [[a, b], [c]] = arr; //コンパイルエラー
// error TS2488: Type 'number[] | undefined' must have a '[Symbol.iterator]()' method that returns an iterator.
これは配列 arr の要素([1, 2] と [3, 4, 5])が number[] | undefined
と推論されるため(undefined の値に分割代入を行うことができないため)エラーが発生しています。
エラーを回避するには分割代入前に要素が配列であること(イテレータを持つこと)を確認または保証するか、undefined にならないようにデフォルト値を設定するなどの方法があるようです。
以下は、分割代入前に [1, 2] と [3, 4, 5] (arr[0] と arr[1])が配列であることを確認する例です。
const arr = [[1, 2], [3, 4, 5]]; // arr[0] と arr[1] が配列であることを確認して分割代入 if (Array.isArray(arr[0]) && Array.isArray(arr[1])) { const [[a, b], [c]] = arr; // OK console.log(a, b, c); // 1 2 3 }
以下は配列 arr に型注釈を付ける例です。
const arr: [number[], number[]] = [[1, 2], [3, 4, 5]]; const [[a, b], [c]] = arr; // OK
以下は undefined にならないようにデフォルト値を設定する例です。
const arr = [[1, 2], [3, 4, 5]]; // デフォルト値を設定 const [[a, b] = [100, 200], [c] = [300]] = arr; console.log(a,b,c); // 1 2 3
また、以下の場合は noUncheckedIndexedAccess を無効にしていてもコンパイルエラーになります。
const arr = [1, 2, ['hello', 'world']]; const [a, b, [c, d]] = arr; //コンパイルエラー //error TS2488: Type 'number | string[]' must have a '[Symbol.iterator]()' method that returns an iterator.
この場合、配列 arr の3番目の要素(['hello', 'world'])が number | string[] と推測され、これが [c, d] に分割代入されようとしているために発生しています。
この場合も、エラーを回避するには分割代入前に3番目の要素が配列であることを確認するなどの方法があります。
以下は分割代入前に3番目の要素(arr[2])が配列であることを確認して分割代入する例です。
const arr = [1, 2, ['hello', 'world']]; if (Array.isArray(arr[2])) { const [a, b, [c, d]] = arr; console.log(a, b, c, d); // 1 2 'hello' 'world' }
以下のように配列 arr に型注釈で明示的に配列の型を指定することができればエラーになりません。
const arr: [number, number, string[]] = [1, 2, ['hello', 'world']]; const [a, b, [c, d]] = arr; console.log(a, b, c, d); // 1 2 'hello' 'world'
デフォルト値(既定値)の指定
配列の分割代入でも、対応する値がない場合のデフォルト値を=
で指定できます。
以下は変数 val4 に取り出す4番目の要素(対応する値)がない場合のデフォルト値を指定する例です。
const arr = [1, 2, 3]; // 4番目の要素に対応する値がない場合のデフォルト値を指定 const [val1, val2, val3, val4 = 4 ] = arr; console.log(val1, val2, val3 + val4); // 1 2 7
但し、この場合も noUncheckedIndexedAccess を有効にしていると、上記のコードの5行目は「'val3' is possibly 'undefined'.」のようなコンパイルエラーになります。
これは noUncheckedIndexedAccess により val3 が number | undefined になるためです。val4 はデフォルト値を設定しているので number 型になっているので、この場合、val3 にもデフォルト値を設定することでエラーを回避できます。
const arr = [1, 2, 3]; const [val1, val2, val3 = 3, val4 = 4 ] = arr; console.log(val1, val2, val3 + val4);
または、以下のように val3 が undefined でないことを確認してもエラーを回避できます。
const arr = [1, 2, 3]; const [val1, val2, val3, val4 = 4 ] = arr; if(val3 !== undefined) { console.log(val1, val2, val3 + val4); // 1 2 7 }
要素をスキップ
特定の要素を取得しないようにするには、変数名を省略する事でスキップすることができます。
const arr = [1, 2, 3, 4, 5, 6]; const [val1, , val3, , , val6] = arr; console.log(val1, val3, val6); //1 3 6
Rest(残余)パターン
Rest パターン...
を利用すると、分割代入で個々の変数に割り当てられなかった残りの要素を1つの配列にまとめることができます。Rest パターンは必ず末尾の要素として指定する必要があります。
配列の Rest パターンは、その位置以降の全ての要素を新たな配列にコピーします。
const arr = [1, 2, 3, 4, 5]; const [a, b, ...c] = arr; console.log(a); // 1 console.log(b); // 2 console.log(c); // [3, 4, 5]
引数
関数の引数がオブジェクトや配列の場合、分割代入構文は関数の引数でも利用することができます。
オブジェクトの場合
引数がオブジェクトの場合の分割代入構文は、引数のカッコの中で {}
にオブジェクトのプロパティ名と同じ名前の引数をカンマ区切りで並べます。
そして分割代入引数の右にオブジェクトの型注釈を記述します。
function foo({ a, b }: { a: number; b: number }) { const result = a + b; console.log(result); } const obj = { a: 1, b: 2, c: 3 } foo(obj); // 3
以下は上記をアロー関数で書き換えたものです。
const foo = ({ a, b }: { a: number; b: number }) => { const result = a + b; console.log(result); }
プロパティ名を別の引数名で受け取るには、:
の後に引数名を指定します。
function foo({ a:x, b:y }: { a: number; b: number }) { const result = x + y; console.log(result); }
既定値とコンパイルエラー
JavaScript では分割代入引数に対応するオブジェクトプロパティがない場合、undefined が代入されますが、TypeScript ではコンパイルエラーが発生します。
以下の場合、JavaScript では a と b に undefined が代入され、result は NaN になりますが、TypeScript ではコンパイルエラーになります。
function foo({ a, b }: { a: number; b: number }) {
const result = a + b;
console.log(result);
}
const empty = {};
foo(empty); // コンパイルエラー
// error TS2345: Argument of type '{}' is not assignable to parameter of type '{ a: number; b: number; }'.
上記の場合、デフォルト引数を指定することで、エラーを回避することができます。
デフォルト引数を型注釈する場合、オブジェクトではプロパティを ?
でオプションにする必要があります。
function foo({ a=0, b=0 }: { a?: number; b?: number }) { const result = a + b; console.log(result); } const empty = {}; foo(empty); // 0
type 文や interface を使って以下のように記述することもできます。
type Foo = { a?: number; b?: number } function foo({ a=0, b=0 }: Foo) { const result = a + b; console.log(result); }
配列の場合
引数が配列の場合の分割代入構文は、引数のカッコの中で []
に配列要素を代入する変数名をカンマ区切りで並べます。配列要素に対応する引数名は任意の名前を指定できます。
そして分割代入引数の右に配列型の型注釈を記述します。
function bar([x, y]: number[]) { const result = x * y; console.log(result); } const arr = [1, 2, 3]; bar(arr); // 2
また、配列の分割代入と同様、変数名を省略して要素をスキップすることができます。
// 1つ目の要素をスキップ function bar([, y, z]: number[]) { const result = y * z; console.log(result); } const arr = [1, 2, 3]; bar(arr); // 6
noUncheckedIndexedAccess
コンパイラオプション noUncheckedIndexedAccess を有効にしている場合、分割代入引数は undefined とのユニオン型になります。
そのため、先述の以下のコードはコンパイルエラーが発生します。
function bar([x, y]: number[]) {
const result = x * y; // コンパイルエラー
console.log(result);
}
// error TS18048: 'y' is possibly 'undefined'.
上記のエラーを回避する1つの方法は配列の分割代入引数のデフォルト引数を指定します。
function bar([x=0, y=0]: number[]) { const result = x * y; console.log(result); } const arr = [1, 2, 3]; bar(arr); // 2
また、配列の分割代入引数の型注釈をタプル型にすると、noUncheckedIndexedAccess が有効な場合でも、undefined とのユニオン型にはなりません。
この場合、配列 arr の型は分割代入引数の型注釈と一致している必要があります(上記の例のように3つの要素を持つ場合はエラーになります)。
// 型注釈をタプル型に function bar([x, y]: [number, number]) { const result = x * y; console.log(result); } // 配列はタプル型 const arr: [number, number] = [1, 2]; bar(arr); // 2
Map
Map はキーと値のペアを取り扱うための JavaScript の組み込みオブジェクトです。Map には1つのキーについては1つの値のみを格納できます。
TypeScript で Map オブジェクトの型は Map<K, V>
のように2つの型引数を取り、Map<K, V>
はキーが K 型、値が V 型の Map オブジェクトの型を表します。
Map を作成するには Map コンストラクタを呼び出します。
Map<string, number> 型の Map(キーが string 型、値が number 型)は以下のように作成することができます。set() メソッドはキーと値を持つ要素を追加します。
// Map<string, number> 型の中身が空の Map オブジェクトを作成 const map = new Map<string, number>(); // キーが foo、値が 123 のペア(要素)を追加 map.set("foo", 123); console.log(map.get("foo")); // 123
以下のように Map 要素の型を変数に指定することもできます。
const map: Map<string, number> = new Map(); // 空の Map オブジェクトを作成
型を指定せずに空の Map オブジェクトを作成した場合、キーと値の型はどちらも any 型に推論されます。
const map = new Map(); // Map<any, any> 型
コンストラクタにキーと値のタプル型 [K, V] の配列を渡すと Map<K, V> オブジェクトが作られます。
const myMap = new Map<string, string>([ ['key1', 'value1'], ['key2', 'value2'] ]); console.log(myMap.get("key2")); // value2
Map の型変数を省略した場合、TypeScript はコンストラクタ引数から Map<K, V> の型を推論します。
以下の場合、Map<number, boolean> と推論されます。
const myMap = new Map([ // 型変数を省略すると引数から型を推論→Map<number, boolean> 型 [101, false], [102, true] ]);
get() メソッドの返り値は V 型または undefined
Map<K, V> 型の get() メソッドは K 型の引数(キー)を受け取り、そのキーで保存された V 型の値を返しますが、指定されたキーに対応する値がない場合は undefined を返します。
つまり、get() メソッドの返り値は V 型または undefined のユニオン型(V | undefined)になります。
そのため、以下の場合、get() メソッドで取得した foo は number | undefined になり、foo*2
は foo が undefined の可能性があるためコンパイルエラーになります。
const map = new Map<string, number>(); map.set("foo", 123); const foo = map.get("foo"); console.log(foo); // 123 console.log(foo *2); // コンパイルエラー // error TS18048: 'foo' is possibly 'undefined'.
以下のように値が undefined でないことを確かめればエラーを回避できます。
if(foo !== undefined) { console.log(foo *2); // 246 } // または foo が number であることを確認 if(typeof foo === "number") { console.log(foo *2); // 246 }
Map<K, V> 型の get() メソッドの返り値は V 型または undefined のユニオン型になりますが、forEach() メソッドや for of 文での反復処理で取得する値は V 型になります。
以下の場合、value は number 型になるのでエラーになりません。
const map = new Map<string, number>(); map.set("foo", 123); map.set("bar", 456); for (const [key, value] of map) { console.log(value * 2); // 246 912 }; map.forEach((value,key) => { console.log(value * 2); // 246 912 });
以下は キーに string 型のユーザーID、値に UserInformation 型のユーザー情報を持つ Map の例です。
type UserInformation = { name: string; age: number; hobby?: string; }; const userMap = new Map<string, UserInformation>(); userMap.set('001', {name: 'John Doe', age: 24, hobby: 'swimming'}) userMap.set('002', {name: 'Jane Smith', age: 32}); console.log(userMap.get('001')); // {name: 'John Doe', age: 24, hobby: 'swimming'} console.log(userMap.get('002')); // {name: 'Jane Smith', age: 32} console.log(userMap.get('003')); // undefined console.log(userMap.has('003')); // false // Map の forEach メソッドで全ての要素を指定したコールバックで挿入順に反復処理 userMap.forEach((value, key) => { console.log(key, value); /* 001 {name: 'John Doe', age: 24, hobby: 'swimming'} 002 {name: 'Jane Smith', age: 32} */ }); let totalAge = 0; // for...of で全ての要素を反復処理 for (const [key, value] of userMap) { // age の合計 totalAge += value.age; }; // age の平均 console.log('Average age is ' + totalAge/userMap.size);
Set
Set は一意な値の集まり(集合)を扱うことができる JavaScript の組み込みオブジェクトです。
Set は配列に似ていますが、インデックスによるアクセスはできず、重複した値を持つことはできません。
また、Map と Set は用途は異なりますが、Set はキーだけの値のない Map と考えることもできます。
TypeScript で Set の型は Set<T>
のように型引数を1つ取ります。
Set を作成するには Set コンストラクタを呼び出します。Set<string> 型の Set(値が string 型)は以下のように作成することができます。
set() メソッドは値を追加します。追加した値は最後に足され、すでに存在する値は追加されず順番は変わりません。
// Set<string> 型の中身が空の Set オブジェクトを作成 const mySet = new Set<string>(); // または const mySet: Set<string> = new Set(); // 値を追加 mySet.add('foo'); console.log(mySet); // Set(1) {'foo'} mySet.add('bar'); console.log(mySet); // Set(2) {'foo', 'bar'} // すでに存在する値は追加されず順番は変わりません mySet.add('foo'); console.log(mySet); // Set(2) {'foo', 'bar'}
型を指定せずに空の Set オブジェクトを作成した場合、Set<unknown> になってしまいます。
const mySet = new Set(); // Set<unknown>
コンストラクタに配列などの反復可能オブジェクトを渡すと、値が Set に格納されます。
const mySet = new Set([1, 2, 3, 4, 5]); // Set<number> console.log(mySet); // Set(5) {1, 2, 3, 4, 5}
以下は User 型の値を持つ Set の例です。users は一意なアイテムのみを含む Set なので、すでに存在する値(User オブジェクト)を追加しようとしても、それは再び追加されません。
type User = { id: number; name: string; }; const user1: User = { id: 1, name: 'foo' } const user2: User = { id: 2, name: 'bar' } const users = new Set<User>(); function addToUsers(user: User) { users.add(user); console.log(`${user.name} has been added to the users.`); } addToUsers(user1); // foo has been added to the users. addToUsers(user2); // bar has been added to the users. addToUsers(user1); // foo has been added to the users.(追加されない) console.log(users); // Set(2) {id: 1, name: 'foo'} と {id: 2, name: 'bar'}
サンプル
以下は JavaScript のコードを TypeScript に書き換える例です。
以下のような各行にユーザー名、年齢、有効かどうかを表す数値(1 または 0)がカンマ区切りで記述された CSV ファイル(src/users.csv)を fetch() を使って取得します。
foo,26,1 bar,22,0 baz,34, 1 // 1 の前にはスペースが入ってしまっている
そして取得したファイルから以下のようなユーザー情報のオブジェクトの配列を作成します。
[ { "name": "foo", // 1つ目の値 "age": 26, // 2つ目の値 "active": true // 3つ目の値(1であれば true、それ以外は false) }, { "name": "bar", "age": 22, "active": false }, { "name": "baz", "age": 34, "active": true } ]
JavaScritp では例えば、以下のように記述することができます(コードの内容はコメントに記載)。
この例ではカンマ区切りで分割した文字列は前後の空白文字を trim() で削除しています。これは、例えば、CSV ファイル(users.csv)の3行目の 1 の前にはスペースが入ってしまっているので、activeString === "1"
で判定すると active は false となってしまうためです。
// オブジェクトを格納する配列を初期化 const users = []; // fetch() で CSV ファイルのデータを取得してテキストに変換 fetch('src/users.csv') .then((response) => response.text()) .then((data) => { // 取得した CSV ファイルのテキストデータを変数に代入 const csv = data; // 改行文字("\n")で1行ごとに分解して配列 lines に代入 const lines = csv.split("\n"); // 配列の要素(1行ごとのデータ)を反復処理 for (const line of lines) { // 空の行であればスキップ if(line === "") { continue; } // 各行の文字列をカンマ区切りで分割したものから配列を作成し、変数に分割代入 const [nameString, ageString, activeString] = line.split(','); // 前後の空白文字を除去して変数 name に代入 const name = nameString.trim(); // 前後の空白文字を除去した値を数値に変換して変数 age に代入 const age = Number(ageString.trim()); // 前後の空白文字を除去して値が1であれば true、それ以外は false にして変数 active に代入 const active = activeString.trim() === "1"; // プロパティの短縮構文でオブジェクトを作成して配列に追加 users.push({name, age, active}); } // 配列の内容を出力(確認) for(const user of users) { console.log(user); /* {name: 'foo', age: 26, active: true} {name: 'bar', age: 22, active: false} {name: 'baz', age: 34, active: true} */ } });
上記をそのまま TypeScript として記述すると以下の赤い字の部分がコンパイルエラーになります。
7行目の引数 data は自動的に string 型と推論され、csv も string 型と推論されます(詳細)。
lines は csv を split() で文字列の配列にして返される値なので string[] 型と推論され、line はその要素なので string 型と推論されます。
[nameString, ageString, activeString] の各要素は、lines を split() で文字列の配列にして返される値を分割代入していますが、型推論により number | undefined になります。そのため、これらの値に対して文字列のメソッド trim() を使用するとコンパイルエラーになります。
// users の型が指定されていないのでコンパイルエラー(any[] になってしまう) const users = []; // Variable 'users' implicitly has type 'any[]' in some locations where its type cannot be determined. fetch('src/users.csv') .then((response) => response.text()) .then((data) => { // 引数 data は string 型と推論される const csv = data; // csv は string 型と推論される const lines = csv.split("\n"); // lines は string[] 型と推論される for (const line of lines) { // line は string 型と推論される if(line === "") { continue; } const [nameString, ageString, activeString] = line.split(','); // nameString が undefined になる可能性があるのでコンパイルエラー const name = nameString.trim(); // nameString' is possibly 'undefined'. // ageString が undefined になる可能性があるのでコンパイルエラー const age = Number(ageString.trim()); // 'ageString' is possibly 'undefined'. // activeString が undefined になる可能性があるのでコンパイルエラー const active = activeString.trim() === "1"; // 'activeString' is possibly 'undefined'. users.push({name, age, active}); } // 変数 'users' の型は暗黙的に 'any[]' になるのでコンパイルエラー // Variable 'users' implicitly has an 'any[]' type. for(const user of users) { console.log(user); } });
以下は上記のコードをコンパイルエラーにならないように、書き換えた例です。
type 文を使って配列 users に格納するオブジェクトの型を User として定義(宣言)しています。
users は User 型の要素を持つ配列なので、型注釈に User[]
と配列型を指定しています。
type 文の代わりに interface を使うこともできます。
tsconfig.json で noUncheckedIndexedAccess を有効にしている場合、21行目の分割代入では、型推論により変数の型が string | undefined
のユニオン型になるので、デフォルト値を指定して変数の型を string 型としています。
//配列 users に格納するオブジェクトの型 User の宣言(型エイリアス) type User = { name: string; age: number; active: boolean; } // 上記で定義した User 型の要素を持つ配列として型注釈 const users: User[] = []; fetch('src/users.csv') .then((response) => response.text()) .then((data) => { const csv: string = data; const lines = csv.split("\n"); for (const line of lines) { if(line === "") { continue; } // 分割代入のデフォルト値を指定して、値を string 型に const [nameString='', ageString='', activeString=''] = line.split(','); // 変数の型を指定(この部分の型注釈は省略可能) const name: string = nameString.trim(); const age: number = Number(ageString.trim()); const active: boolean = activeString.trim() === "1"; users.push({name, age, active}); } // users は User[] for(const user of users) { console.log(user); } });
または、以下のように分割代入された値が undefined でないことを確認すれば、代入時にコンパイルエラーにはなりません。
fetch('src/users.csv') .then((response) => response.text()) .then((data) => { const csv: string = data; const lines = csv.split("\n"); for (const line of lines) { if(line === "") { continue; } const [nameString, ageString, activeString] = line.split(','); // 値が undefined でなければ if(nameString !== undefined && ageString !== undefined && activeString !== undefined) { const name: string = nameString.trim(); const age: number = Number(ageString.trim()); const active: boolean = activeString.trim() === "1"; users.push({name, age, active}); } } for(const user of users) { console.log(user); } });
上記は以下のように記述してもほぼ同じことです。
fetch('src/users.csv') .then((response) => response.text()) .then((data) => { const csv: string = data; const lines = csv.split("\n"); for (const line of lines) { if(line === "") { continue; } const [nameString, ageString, activeString] = line.split(','); // 値が truthy であれば(undefined など false でなければ) if(nameString && ageString && activeString ) { const name: string = nameString.trim(); const age: number = Number(ageString.trim()); const active: boolean = activeString.trim() === "1"; users.push({name, age, active}); } } for(const user of users) { console.log(user); } });
また、以下のように分割代入された値が string 型であることを確認する(絞り込む)方法もあります。
fetch('src/users.csv') .then((response) => response.text()) .then((data) => { const csv: string = data; const lines = csv.split("\n"); for (const line of lines) { if(line === "") { continue; } const [nameString, ageString, activeString] = line.split(','); // 各値が string 型であれば(string 型に絞り込む) if(typeof nameString === "string" && typeof ageString === "string" && typeof activeString === "string"){ // ここの中では各値は string 型 const name: string = nameString.trim(); const age: number = Number(ageString.trim()); const active: boolean = activeString.trim() === "1"; users.push({name, age, active}); } } for(const user of users) { console.log(user); } });
以下は引数に受け取った値が string 型であるかどうかを判定するユーザー定義型ガードを定義して利用する例です。
// ユーザー定義型ガード関数 function isString(value: unknown): value is string { return typeof value === "string"; } fetch('src/users.csv') .then((response) => response.text()) .then((data) => { const csv: string = data; const lines = csv.split("\n"); for (const line of lines) { if(line === "") { continue; } const [nameString, ageString, activeString] = line.split(','); // ユーザー定義型ガードで string 型であることを確認 if(isString(nameString) && isString(ageString) && isString(activeString)) { const name: string = nameString.trim(); const age: number = Number(ageString.trim()); const active: boolean = activeString.trim() === "1"; users.push({name, age, active}) } } for(const user of users) { console.log(user); } });
fetch() の補足説明
fetch() メソッドは Response オブジェクトを結果に持つ Promise オブジェクトを返し、fetch() メソッドが返す Promise の型は型引数を持つ Promise<Response>
型として表せます。
通常、then() メソッドのコールバックの引数の型は省略できますが、型注釈を書くこともできます。
最初の then() メソッドのコールバックの引数 response は fetch() が返す Promise の結果なので、Response 型になります。
そして Response オブジェクトの text() メソッドは 文字列として解決するPromise<string>
型の Promise を返すので、data は string 型になります。
String のメソッド split() は指定された区切り文字で文字列を分割して string[] 型の配列を返します。
fetch('users.csv') // 引数 response は Response 型のオブジェクト .then((response: Response) => response.text()) // 引数 data は string 型 .then((data: string) => { // csv は string 型 const csv = data; // lines は string[] 型 const lines = csv.split("\n"); // ... });