JavaScriptの文法を理解している人を対象にTypeScriptの導入と型の基本文法の習得をできることを目的とします。
この記事では簡易のため、ts-nodeを使ってTypeScript環境の構築と動作確認を行います。
TypeScriptについて
TypeScriptはマイクロソフトによって開発されているオープンソースのプログラミング言語です。
TypeScriptはJavaScriptに対して、省略も可能な静的型付けとクラスベースオブジェクト指向を加えた厳密なスーパーセットとなっています。(JavaScriptの文法を完全包括していて、JavaScriptに後付で型定義ができます。)
C#のリードアーキテクトのアンダース・ヘルスバーグがTypeScriptの開発に関わっています。
TypeScriptはクライアントサイド、あるいはサーバサイド (Node.js) で実行されるJavaScriptアプリケーションの開発に利用できます。
TypeScriptは大規模なアプリケーションの開発のために設計されています。
メリット
- 間違ったパラメータを渡さないようにチェックできたり、パラメータの渡し忘れをチェックできる
- undefined、nullが混在しない(strictNullChecksをtrueにしてる場合)
- lintがより厳しくなる(コンパイルレベルのエラーになるため)
- 補完が強化される(interface定義している場合)
- 型定義を見れば、パラメータの型がわかる(型定義を作り込んでいる場合、インターフェース定義書にもなる)
デメリット
- 型定義を覚えるという学習コストが増える(自由度が高く、型定義の実装方法はプログラマに委ねられるため、学習量はかなり多い)
- プロダクトの本質でない型定義のエラー解消に時間を取られる場合がある(特にジェネリックス型にconditonal typesは組み合わさると可読性が悪い・・・)
- 型定義の記述が必要なため、コードの記述量が多くなる(が、その分補完も効くようにはなる)
- コンパイル済みのコードは結局JSなので、実行時エラーは完全には防げない(過度な期待は禁物)
- サードパーティ型定義ライブラリに苦しむ(型が一致しない場合があると)
- namespaceを気をつけないと型定義汚染する(グローバルスコープ)
サードパーティライブラリの型定義ファイルに関しては
型定義ファイルをまとめたリポジトリ(DefinitelyTyped)というものがあります。
https://github.com/DefinitelyTyped/DefinitelyTyped
ただし、TypeScriptのDefinitelyTypedは「ダメでもともと、うまく使えればラッキー」くらいの距離感がよい
に書いてあるとおり、元ライブラリのメンテナーが必ずしも型定義をしているわけでもないようです。
そのため、ライブラリのバージョンを上げた際に型定義が合わなくなるという問題が発生します。
(その場合はそのライブラリの型定義を使わないという選択肢もありだと思います)
要は型定義を過度に頑張らないのが運用していく上で前提としてある気がします。
TypeScript再入門 ― 「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に
かといって何でもかんでもany型にすると意味がないのですが・・・
(ライブラリ以外での自前のデータ構造をできる限り型定義しておくと保守性あがるよということです)
TypeScript環境構築
typescriptのコンパイル設定(tsconfig.json)を生成します。
$ tsc --init
今回は生成されたtsconfig.jsonを次の設定にして使います。
参考:tsconfig 日本語訳
{
"compilerOptions": {
"target": "ES2019", /* ECMAScriptのターゲットバージョンを指定します: 'ES3'(デフォルト), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018'または 'ESNEXT' */
"module": "commonjs", /* モジュールコード生成: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015',または 'ESNext'を指定します。 */
"strict": true, /* すべての厳密な型チェックオプションを有効にします。 */
"noImplicitAny": true, /* 暗黙の 'any'型で式と宣言のエラーを発生させます。 */
"strictNullChecks": true, /* 厳密なヌルチェックを有効にします。 */
"noImplicitThis": true, /* 暗黙の 'any'型で 'this'式のエラーを発生させます。 */
"esModuleInterop": true /* デフォルトのエクスポートがないモジュールからのデフォルトのインポートを許可します。これはコードの出力には影響しません。型検査だけです。 */
}
}
typescript用パッケージとeslintパッケージを含むpackage.jsonを作成します。
{
"name": "typescript",
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"scripts": {
"start": "npx ts-node index.ts"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"eslint": "^6.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-prettier": "^3.1.0",
"prettier": "^1.18.2"
},
"dependencies": {
"ts-node": "^8.3.0",
"typescript": "^3.5.3"
}
}
文法チェック用に.eslintrc.js
を作成します。
参考:[VSCodeでESLint+@typescript-eslint+Prettierを導入する]
(https://qiita.com/madono/items/a134e904e891c5cb1d20)
module.exports = {
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"plugins": [
"@typescript-eslint"
],
"parser": "@typescript-eslint/parser",
"env": { "browser": true, "node": true, "es6": true },
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
}
}
依存パッケージのインストールを行います。
$ npm install
もしくはyarnインストール済みの場合
$ yarn
適当なindex.tsを作成します。
const a: number = 3;
const b: string = "hello world!";
const c: boolean = true;
console.log(a);
console.log(b);
console.log(c);
npx経由からts-nodeでindex.tsをコンパイル&実行します
$ npx ts-node index.ts
3
hello world!
true
実行できれば最低限の環境構築は完了です。
TypeScript文法
typescript文法参考
・TypeScript公式
・TypeScriptの型入門
今回のサンプルのGitHubを用意しました(TypeScript 3.5)
(https://github.com/teradonburi/typescript)
基本的に公式のハンズオンに書かれているサンプルと説明の意訳です。
strictモードでコンパイルエラーになる箇所は修正したり、説明が足りない箇所は補足したりしてます。
基本型(Basic Types)
Enum、NeverあたりがTypeScriptの完全な独自型です。
Boolean型
true, falseのみの型です。
let isDone = false;
console.log(isDone);
Number型
数値の型です。
let decimal = 6; // 10進数
let hex = 0xf00d; // 16進数
let binary = 0b1010; // 2進数
let octal = 0o744; // 8進数
console.log(decimal);
console.log(hex.toString(16));
console.log(binary.toString(2));
console.log(octal.toString(8));
String型
文字列の型(ダブルクォート、シングルクォート、バッククォートで囲まれた文字列)です。
let color = "blue";
color = "red";
const red = "#ff0000";
color = `${red}`;
console.log(color);
Array型
配列の型です。
let list: number[] = [1, 2, 3];
// Array<number>のジェネリクスで書くこともできる
//let list: Array<number> = [1, 2, 3];
console.log(list);
Tuple型
別の型が入っている配列の場合は配列内の中身の型を定義する必要があります。
順番、要素数が制約されるため、制約としてはかなり厳しいです。
let x: [string, number];
// 初期化
x = ["hello", 10]; // OK:宣言の型と順番が一致する必要がある
// x = [10, "hello"]; // エラー:宣言の型と順番が一致する必要がある
console.log(x);
また、オプショナル指定することや
ジェネリックスで可変長引数にタプル型をそのまま渡すことも可能です。
より実践的な使い方はこちらが参考になります。
Enum型
C、Java、C#のように列挙体を定義することが可能(JavaScriptには無い、TypeScript拡張)です。
// 列挙型(省略した場合、0からインクリメントされた数値が列挙される)
enum Color1 {
Red, // 0
Green, // 1
Blue // 2
}
let c1: Color1 = Color1.Green;
console.log(c1); // 1
// 個別に数値を振ることも可能
enum Color2 {
Red = 1, // 0
Green = 2, // 2
Blue = 4 // 4
}
let c2: Color2 = Color2.Blue;
console.log(c2); // 4
// 文字列も指定できる
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
const dir: Direction = Direction.Up;
console.log(dir);
// 列挙化型に割り当てられる値
// 1.リテラル列挙式(基本的には文字列リテラルまたは数値リテラル)
// 2.以前に定義された定数列挙型メンバーへの参照(異なる列挙型から発生する場合があります)
// 3.括弧で囲まれた定数列挙式
// 4.定数列挙式に適用される+、-、〜単項演算子のいずれか
// 5.+、-、*、/、%、<<、>>、>>>、&、|、^オペランドとして定数列挙式を持つ二項演算子
// 定数列挙式がNaNまたはInfinityに評価されると、コンパイル時エラーになります。
enum FileAccess {
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
G = "123".length
}
console.log(FileAccess);
Any型
どんな型でも入れることができる型です。
サードパーティの型定義がないライブラリを使うときや、既存のJSプロジェクトをマイグレーションする時に主に使います。
型定義の意味がなくなるので、最終的にはリファクタリングすべき(使わないようにする)
TypeScriptのコンパイルチェックが実質体をなさなくなるので、実行時にエラーになる可能性があります。
let notSure: any = 4;
// notSure.ifItExists(); // Error:コンパイルチェックは通るが、ifItExistsは存在しないので実行時エラーになる
notSure.toFixed(2); // OK:このタイミングではnotSureは数値型なので実行できる(ただし、コンパイルチェックはされない)
notSure = "maybe a string instead";
notSure = false;
console.log(notSure);
// any型の配列は様々な型が入っているデータが許されてしまう
let li: any[] = [1, true, "free"];
li[1] = 100;
敗北者のTypeScriptに書かれている通り
TypeScriptの享受を最大限得るためには厳密な型チェックに沿って開発する必要があります。
- strict: 厳密な型チェックを行う
- noImplicitAny: 暗黙のanyはエラーにする
- strictNullChecks: undefined, nullを混在させない
- asを使わない(キャストすることで型チェックから逃れてしまう)
- unknown、isを正しく使う
とはいえ、JSからマイグレーションするようなプロジェクトはこの目標はかなり大変なので目標レベルにしましょう
(特にnoImplicitAnyとas縛りはかなり厳しい)
関数型
引数の型とコロン:の後ろは戻り値の型を指定します。
function add(x: number, y: number): number {
return x + y;
}
console.log(add(1, 2));
?
で省略可能な引数を指定できます。
// ?で省略可能な引数を指定できる
function buildName(firstName: string, lastName?: string): string {
if (lastName) return firstName + " " + lastName;
else return firstName;
}
const result1 = buildName("Bob", "Adams"); // OK
// const result2 = buildName("Bob", "Adams", "Sr."); // エラー:引数が多すぎます
const result3 = buildName("Bob"); // OK
console.log(result1);
console.log(result3);
可変長引数の場合、配列型を指定します。
function concatName(firstName: string, ...restOfName: string[]): string {
return firstName + " " + restOfName.join(" ");
}
// 関数型の変数
const buildNameFun: (fname: string, ...rest: string[]) => string = concatName;
console.log(buildNameFun("FirstName", "MiddleName", "LastName"));
Void型
関数の戻り値を返さないときに指定する型です。
それ以外の使いみちは基本的にありません。
function warnUser(): void {
console.log("This is my warning message");
}
warnUser();
// void型の変数を作成する意味はない(undefinedもしくはnullしか割り当てができないため)
let unusable: void = undefined;
// unusable = null; // --strictNullChecksオプションがついている場合、undefinedとnullの混在はできない
console.log(unusable);
undefined型、null型
単独でundefined、nullしか割り当てできない型ですべての型のサブタイプです。
それゆえ実質単独での使いみちはあまりないですが、共有体型で連結して使う方法があります。
// let u: undefined = undefined;
// let n: null = null;
// console.log(u);
// console.log(n);
// デフォルトでは、nullおよびundefinedは他のすべての型のサブタイプです。
// つまり、数値のようなものにnullとundefinedを割り当てることができます。
// ただし、-strictNullChecksフラグを使用する場合、nullおよびundefinedは許されません。
// これにより、多くの一般的なエラーを回避できます。
// let num: number = 1;
// num = null; // エラー:-strictNullChecksフラグを使用する場合は数値型にnull代入が許されない
// console.log(num);
// 文字列、null、または未定義のいずれかを渡したい場合は、共有体型を使用できます。
// 共有体型は|で代入されうる型を連結させます。
let str: string | null | undefined = undefined;
str = null;
str = "文字列";
console.log(str);
Never型
never型は常に例外をスローする関数または例外でreturn文が呼ばれない関数の戻り値として使います。
never型はすべての型のサプタイプですが、値を割り当てすることはできません。
// 関数がreturnに到達せず、常に例外を返す場合にnever型を使います。
function error(message: string): never {
throw new Error(message);
}
try {
error("エラーです");
} catch (error) {
console.log(error);
}
// 実際使う場合は意識する必要はあまりない(すべての型のサブタイプなので共有体型にする必要もない)
function exec(message: string): string {
if (message === "err") {
throw new Error(message);
}
return message;
}
console.log(exec("テスト"));
Object型
Object型は非プリミティブ型です。
つまり、number、string、boolean、symbol、nullまたはundefinedではないものを表す型です。
Object型を使用すると、Object.createなどのAPIをより適切に表現できます。
function create(o: object): void {
console.log(o);
}
create({ prop: 0 }); // OK
// create(42); // エラー
// create("string"); // エラー
// create(false); // エラー
// create(null); // エラー
// create(undefined); // エラー
TypeScriptにはObject、{}、object型の3つの型が存在しますが
Object、{}、object型の違いは次の記事が参考になります。
参考:Object、{}、object型の違い
let o1: Object;
let o2: object;
let o3: {};
通常よく使うのはo3: {}ですが、部分的構造化による弱い型付けのため、意図しない型が入る可能性があります。
参考:object型と{}型
そのため、後述のinterfaceを使ったほうが良いです。
型アサーション
型アサーションは、他の言語での型キャストに似ていますが、違う点はデータの特別なチェックや再構築は行いません。
(キャスト後の型が本当に正しいかはプログラマに委ねられているため、実行時エラーになる可能性がある)
ランタイムに影響はなく、純粋にコンパイラーによって使用されます。
主にその場でコンパイルを通したいときなどに使えますが、後でリファクタリングしたほうが良い場合が多いです。
let someValue: any = "this is a string";
// any型をasスタイルでstring型に型キャストする
let strLength: number = (someValue as string).length;
// 次のように山括弧で囲うスタイルもあるが、JSXではasスタイルでの型キャストしか許可されていない
// let strLength: number = (<string>someValue).length;
console.log(strLength);
クラス(Classes)
JavaScriptにはない、TypeScript固有のクラスの機能について紹介します。
アクセス制御子
アクセス制御子が使えます。(private、protected、public)
JavaScriptのクラスはpublicがデフォルトのため、
private、protectedはTypeScript固有の機能です。
とはいえ、privateに関してはJSの方でもChromeなどでサポートされ始めたので
TypeScriptはコンパイルレベルで意図しないところからのアクセスを防ぐという意味合いが大きいです。
// public, protected, privateのアクセス制御子が使える
// protected, privateはTypeScript固有の機能
class Person {
protected name: string; // 継承先からは参照できるけど、外部公開はしない
public constructor(name: string) {
this.name = name;
}
}
class Employee extends Person {
private department: string;
public readonly address: string = "Tokyo"; // readonly
public constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch(): string {
// protectedメンバとprivateメンバは参照できる
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// console.log(howard.department); // エラー:departmentはprivateメンバなため、外部から参照できない
// console.log(howard.name); // エラー:nameはprotectedメンバなため、外部から参照できない
console.log(howard.address); // OK:addressはpublicメンバなため、外部から参照できる
// howard.address = "Osaka"; // エラー:readonlyなため代入できない
抽象クラス(Abstract Classes)
abstractキーワードを使うことで継承後のクラスで実装予定のメンバ変数やメソッドを定義できます。
抽象クラスはインスタンス化できませんが、継承後のクラスをダウンキャストして使うことができます(実装の隠蔽化、ポリモーフィズム)。
他のオブジェクト指向言語のおなじみの機能です。
// 抽象クラス
abstract class Department {
private name: string;
public constructor(name: string) {
this.name = name;
}
public printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // must be implemented in derived classes
}
class AccountingDepartment extends Department {
public constructor() {
super("Accounting and Auditing"); // constructors in derived classes must call super()
}
public printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
public generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // OK:抽象クラスの参照はできる
// department = new Department(); // エラー: 抽象クラスのインスタンスが生成できない
department = new AccountingDepartment(); // OK: 非中朝クラスのインスタンスは生成でき、抽象クラスにダウンキャストする
department.printName();
department.printMeeting();
// department.generateReports(); // エラー: このメソッドは抽象クラスでは定義されていない
インターフェース(Interfaces)
TypeScriptの中核となる原則の1つは、型チェックが値の持つ形状に焦点を当てることです。
これは「ダックタイピング」または「構造的部分型」と呼ばれることもあります。
TypeScriptでは、インターフェースはこれらの型に名前を付ける役割を果たし、
プロジェクト内のコードとの取り決めだけでなく、コード内の取り決めを定義する強力な方法です。
// インターフェース定義をしない場合
function printLabel1(labeledObj: { label: string }): void {
console.log(labeledObj.label);
}
let myObj1 = { size: 10, label: "Size 10 Object" };
printLabel1(myObj1);
// インターフェース定義をする場合
// インターフェース定義をするにはinterfaceキーワードを使う
interface LabeledValue {
label: string; // プロパティ名と型を定義していく
}
function printLabel2(labeledObj: LabeledValue): void {
console.log(labeledObj.label);
}
let myObj2 = { size: 10, label: "Size 20 Object" };
printLabel2(myObj2);
オプショナルなプロパティを持つinterface
オプショナルなプロパティの利点は、これらの利用可能なプロパティを記述できると同時に、インターフェースの一部ではないプロパティの使用を防止できることです。
interface SquareConfig {
color?: string; // 省略可能なプロパティは?をつける
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
// エラー: clorというプロパティはSquareConfigに定義されていない
// newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({ color: "black" });
console.log(mySquare);
readonly
プロパティにはreadonly属性をつけることができます。
readonlyされたプロパティは代入できません。
// readonlyなプロパティ
interface Point {
readonly x: number;
readonly y: number;
}
const p1: Point = { x: 10, y: 20 }; // 初期化時はOK
// p1.x = 5; // エラー!
console.log(p1);
readonlyな配列
配列にreadonlyをつけることで変更操作を一切封じることができます。
let a: number[] = [1, 2, 3, 4];
let ro: readonly number[] = a;
// ro[0] = 12; // エラー:要素に代入させない!
// ro.push(5); // エラー:配列に要素に影響を与えるメソッドは呼べない!
// ro.length = 100; // エラー:配列プロパティに代入させない!
// a = ro; // エラー:readonlyでない配列に代入させない!
// 通常、readonlyな配列を通常の配列に代入するのは違法だけど、型アサーションでキャストすることはできる
a = ro as number[];
// readonlyまたはconstのどちらを使用するかを覚える最も簡単な方法は、
// 変数で使用するかプロパティで使用するかを考えることです。
// 変数はconstを使用しますが、プロパティはreadonlyを使用します。
// 過剰プロパティチェック
// 型 '{ colour: string; width: number; }' の引数を型 'SquareConfig' のパラメーターに割り当てることはできません。
// オブジェクトリテラルで指定できるのは既知のプロパティのみですが、'colour' は型 'SquareConfig' に存在しません。書こうとしたのは 'color' ですか?
// let mySquare2 = createSquare({ colour: "red", width: 100 }); // エラー:存在しないプロパティを指定した場合にエラー
// 型アサーションでキャストして回避する方法もあるが・・・
//let mySquare3 = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
// オブジェクトに特別な方法で使用される追加のプロパティがあることが確実な場合は、文字列インデックスシグネチャを使う
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any; // 文字列インデックスシグネチャ
}
これらのチェックを回避する最後の方法は、少し驚くかもしれませんが、オブジェクトを別の変数に割り当てることです。
squareOptionsは過剰なプロパティチェックを受けないため、コンパイラはエラーを表示しません。
let squareOptions = { colour: "red", width: 100 };
let mySquare4 = createSquare(squareOptions);
console.log(mySquare4);
上記のような単純なコードの場合、これらのチェックを「回避」しようとすべきではないことに注意してください。
メソッドを持ち、状態を保持するより複雑なオブジェクトリテラルの場合、
これらのテクニックを覚えておく必要があるかもしれませんが、
過剰なプロパティエラーの大抵はバグです。
color、colourの両方をSquareConfigに渡してよい場合はinterface側で定義すべきです。
関数型(Function Types)
関数もinterface内に定義できます。引数の型とコロン:の後ろは戻り値の型を指定します。
interface SearchFunc {
(source: string, subString: string): boolean;
}
定義したら、この関数型のインターフェースを他のインターフェースと同じように使用できます。
ここでは、関数型の変数を作成して、同じ型の関数値を割り当てる方法を示します。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
};
console.log(mySearch("Hello World!", "or"));
インデックス可能な型(Indexable Types)
インターフェースを使用して関数型を記述する方法と同様に、
a[10]やageMap["daniel"]のように「インデックスを付ける」ことができる型を記述することもできます。
// インデックスシグネチャを持つStringArrayインターフェースがあります。
// このインデックスシグネチャは、StringArrayが数値でインデックス付けされると、文字列を返すことを示しています。
interface StringArray {
[index: number] : string;
}
let myArray1: StringArray;
myArray1 = ["Bob", "Fred"];
let myStr: string = myArray1[0];
console.log(myStr);
サポートされているインデックスシグネチャには、文字列と数値の2種類があります。
両方のタイプのインデクサーをサポートすることは可能ですが、
数値インデクサーから返される型は、文字列インデクサーから返される型のサブタイプでなければなりません。
これは、数値でインデックスを作成する場合、JavaScriptは実際にオブジェクトにインデックスを作成する前に文字列に変換するためです。
つまり、100(数字)でのインデックス作成は "100"(文字列)でのインデックス作成と同じことなので、2つは一貫している必要があります。
class Animal {
public name: string;
public constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
public breed: string;
public constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
interface NotOkay {
// エラー:数値インデクサーは文字列インデクサーのサブタイプでなければならない
// [x: number] : Animal;
[x: string] : Dog;
}
// 数値インデクサーが文字列インデクサーのサブタイプなのでOK
// そもそもインデクサーをサブタイプで異なったものを定義する場合はあまりないと思うが・・・
interface Okay {
[x: string] : Animal;
[x: number] : Dog;
}
// 文字列インデックスシグニチャは、「辞書」パターンを記述する強力な方法ですが、すべての他のプロパティが戻り値の型と一致することを強制します。
// これは、文字列インデックスがobj.propertyがobj["property"]としても利用可能であると宣言しているためです。
// 次の例では、nameの型は文字列インデックスの型と一致せず、型チェッカーはエラーを返します。
interface NumberDictionary {
[index: string] : number;
length: number; // OK:lengthはnumber
// name: string; // エラー:nameはインデクサーのサブタイプではない
}
// ただし、インデックスシグネチャが共有体型である場合、異なる型のプロパティは受け入れられます。
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // OK:lengthはnumber
name: string; // OK:nameはstring
}
// インデックスへの割り当てを防ぐために、インデックスシグネチャをreadonlyにすることができます
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray2: ReadonlyStringArray = ["Alice", "Bob"];
//myArray2[2] = "Mallory"; // エラー:readonlyなので代入できない
console.log(myArray2);
クラスの型(Class Types)
C#やJavaなどの言語のインターフェースの最も一般的な使用法の1つである、
クラスがインターフェースの実装を満たすことを明示的に強制することは、TypeScriptでも可能です。
interface ClockInterface1 {
currentTime: Date;
setTime(d: Date): void;
}
// implementsキーワードでinterfaceを実装する
// currentTimeプロパティの存在とsetTimeの実装が保証される
class Clock implements ClockInterface1 {
public currentTime: Date = new Date();
public setTime(d: Date): void {
this.currentTime = d;
}
public constructor() {}
}
const clock = new Clock();
clock.setTime(new Date());
console.log(clock.currentTime);
コンストラクタシグネチャ
コンストラクタの引数を定義することができます。
クラスにインターフェースを使用する場合、クラスにはstatic型とインスタンス型の2つの型があります。(別々で定義する)
クラス式で書くとシンプルにかけます。
// インスタンス型
interface ClockConstructor {
// コンストラクタシグネチャはnewキーワードで定義する
// static型を戻り値で返すようにする
new (hour: number, minute: number): ClockInterface2;
}
// static型(クラスのプロパティの型)
interface ClockInterface2 {
tick(): void;
}
// クラス式で定義する
const Clock2: ClockConstructor = class Clock implements ClockInterface2 {
private h: number;
private m: number;
public constructor(h: number, m: number) {
this.h = h;
this.m = m;
}
public tick(): void {
console.log("beep beep");
}
};
// const clock2 = new Clock2(); // エラー:コンストラクターの引数が一致しない
const clock2 = new Clock2(10, 30);
clock2.tick();
console.log(clock2);
インターフェースの継承(Extending Interfaces)
インターフェースを継承することで既存のインターフェースを再利用し、一部のプロパティの型のみを拡張することができます。
interface Shape {
color?: string;
}
interface PenStroke {
penWidth?: number;
}
// extendsキーワードで継承したいinterfaceを指定します(,区切りで複数interface指定可能)
interface Square extends Shape, PenStroke {
sideLength?: number;
}
let square: Square = {};
square.color = "blue";
square.penWidth = 5.0;
square.sideLength = 10;
console.log(square);
ハイブリッド型(Hybrid Types)
インターフェースは実際のJavaScriptに存在する豊富な型を記述することができます。
JavaScriptの動的で柔軟な性質により、いくつかの型の組み合わせとして機能するオブジェクトに遭遇することがあります。
例えば、関数自体にメソッドやプロパティが定義されている、関数オブジェクトがハイブリット型に該当します。(ES6以前のクラスを使わない書き方など)
サードパーティのJavaScriptとやり取りする場合、ハイブリット型を使用して、型の形状を完全に記述する必要があります。
interface Counter {
// 関数オブジェクト(ハイブリット型)
(start: number): string;
// 関数オブジェクト内のプロパティやメソッド
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function(start: number): string {
return `${start}です`;
} as Counter;
counter.interval = 123;
counter.reset = function(): void {
console.log("reset");
};
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
console.log(c);
インターフェースのクラス継承(Interfaces Extending Classes)
インターフェース型がクラス型を拡張すると、クラスのメンバーは継承されますが、実装は継承されません。
インターフェースが実装を提供せずにクラスのすべてのメンバーを宣言したかのように振る舞います。
インターフェースは、基本クラスのprivate,protectedされたメンバーも継承します。
クラス継承したインターフェースを利用する場合は親クラスの継承も必須化されます。
class Control {
protected state: string = "hello";
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
public select(): void {
console.log(this.state);
}
}
// エラー:Controlクラスの継承が必須のため、stateがないと怒られる
// class IconButton implements SelectableControl {
// public select(): void {
// console.log(this.state);
// }
// }
const button = new Button();
button.select();
ジェネリックス
型定義がわからない型を扱うにはanyを使う必要がありました。
ただし、anyには元の型の情報が失われるという問題がありました。(TypeScriptの意味がない)
// すべての型に対応するためにはany型の引数を渡せばよいですが、型の情報が失われます。
function identity1(arg: any): any {
return arg;
}
console.log(identity1("hello"));
ジェネリックスを使うと型の情報は失われません。
山括弧(<>)で囲まれたジェネリックス型パラメーターリストを関数に指定します。
Tに型の情報が入り、コンパイル時に型は推論され決定されます。
(Tは何でも構いませんがtemplateの意味を取って慣習的にTが使われる事が多いです。複数ジェネリックス型パラメーターリストがある場合は<T, U, V, W>
のように指定します)
Java、C#のジェネリックスと同等のものです。
function identity2<T>(arg: T): T {
return arg;
}
console.log(identity2<string>("world")); // stringを明示的に指定して渡す
console.log(identity2(2)); // 暗黙の型推論に任せる(複雑な場合はコンパイラが型推論に失敗する場合があるのでその場合は明示的に指定する)
配列のジェネリックス
配列のジェネリックスを示すにはT[]
のように記述します。
function loggingIdentity1<T>(arg: T[]): T[] {
console.log(arg.length); // Array型なのでlengthを必ず持つことが保証されている
return arg;
}
console.log(loggingIdentity1(["a", "b", "c"]));
// こう書いても同じです
// function loggingIdentity<T>(arg: Array<T>): Array<T> {
// console.log(arg.length); // Array型なのでlengthを必ず持つことが保証されている
// return arg;
// }
ジェネリックス型(Generics Type)
ジェネリックス関数の型は、非ジェネリック関数の型とまったく同じです。
関数宣言と同様に、変数にジェネリックス関数の型パラメーターを指定します。
function identity3<T>(arg: T): T {
return arg;
}
const myIdentity1: <T>(arg: T) => T = identity3;
console.log(myIdentity1("テスト1"));
// 型変数の数と型変数が揃っている限り、型のジェネリック型パラメーターに別の名前を使用することもできます。
const myIdentity2: <U>(arg: U) => U = identity3;
console.log(myIdentity2("テスト2"));
// ジェネリックス型をオブジェクトリテラル型の呼び出しシグネチャとして書くこともできます。
const myIdentity3: { <T>(arg: T): T } = identity3;
console.log(myIdentity3("テスト3"));
// ジェネリックスのinterface、オブジェクトリテラルをinterface内に記述する
interface GenericIdentityFn1 {
<T>(arg: T): T;
}
const myIdentity4: GenericIdentityFn1 = identity3;
console.log(myIdentity4("テスト4"));
同様の例で、ジェネリックスパラメーターをインターフェース全体のパラメーターに移動することもできます。
これによりインターフェース内の他のメンバが型パラメーターを参照することができるようになります。
interface GenericIdentityFn2<T> {
(arg: T): T;
}
// 明示的に型を指定する必要あり
const myIdentity5: GenericIdentityFn2<string> = identity3;
console.log(myIdentity5("テスト5"));
ジェネリックスクラス(Generic Classes)
ジェネリックスクラスは、ジェネリックスインターフェースに似ています。
ジェネリックスクラスには、クラス名の後に山括弧(<>)で囲まれたジェネリックス型パラメーターリストがあります。
class GenericNumber<T> {
private zeroValue: T;
public add: (x: T, y: T) => T;
public constructor(zeroValue: T, add: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = add;
}
}
// この例では数値用にジェネリックスクラスを生成していますが、文字列などの利用もできます
let myGenericNumber = new GenericNumber<number>(0, (x, y): number => x + y);
console.log(myGenericNumber);
ジェネリックスクラスは、静的側ではなくインスタンス側でのみジェネリックであるため、
クラスを操作する場合、静的メンバはクラスの型パラメーターを使用できません。
なお、ジェネリックスインターフェースとジェネリックスクラスは名前空間を作成することはできません。
また、ジェネリックスの列挙型は存在しません。
ジェネリックスの制約(Generic Constraints)
ジェネリックス型にそのメンバを持つ保証がない場合はコンパイルエラーとなります。
function loggingIdentity2<T>(arg: T): T {
console.log(arg.length); // T型がlengthを持つ保証がないのでエラーになる
return arg;
}
そこでlengthを持つという制約を記述するインターフェースを作成します。
ここでは、単一の.lengthプロパティを持つインターフェースを作成し、
このインターフェースとextendsキーワードを使用して制約を示します。
keyofはオブジェクトのキーを示します。
interface Lengthwise {
length: number;
}
function loggingIdentity3<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Lengthwiseにはlengthを持つことが保証されているのでエラーにならない
return arg;
}
// console.log(loggingIdentity2(3)); // エラー: lengthが存在していない
loggingIdentity3({ length: 10, value: 3 }); // OK: lengthが存在している
ジェネリックス制約での型パラメーターの使用(Using Type Parameters in Generic Constraints)
別の型パラメーターによって制約される型パラメーターを宣言できます。
たとえば、ここでは、キー名が指定されたオブジェクトからプロパティを取得します。
objに存在しないプロパティを誤って取得しないように、2つの型の間に制約を設定します。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[keyof T] {
return obj[key];
}
let y = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(y, "a")); // OK
// console.log(getProperty(y, "m")); // エラー: mは 'a' | 'b' | 'c' | 'd'に割り当てられていません
ジェネリックスのクラス型での利用(Using Class Types in Generics)
ジェネリックを使用してTypeScriptでファクトリークラスを作成する場合、
コンストラクター関数でクラス型を参照する必要があります。
class Test {
public constructor() {
console.log("hello");
}
}
// クラス型を引数に渡す
function createClassInstance<T>(c: { new (): T }): T {
return new c(); // インスタンス生成
}
// こちらでも同じ
// function create<T>(c: new () => T ): T {
// return new c(); // インスタンス生成
// }
console.log(createClassInstance(Test));
より高度な例では、prototypeプロパティを使用して、
コンストラクター関数とクラス型のインスタンス側との関係を推測および制約します。
class BeeKeeper {
public hasMask: boolean = false;
}
class ZooKeeper {
public nametag: string = "";
}
class PlantKeeper {
public nametag: string = "";
}
class Anim {
public numLegs: number = 0;
}
class Bee extends Anim {
public keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Anim {
public keeper: ZooKeeper = new ZooKeeper();
}
class Plant {
public keeper: PlantKeeper = new PlantKeeper();
}
// Anim型を継承したクラスのインスタンスを返す関数
function createInstance<A extends Anim>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // OK
createInstance(Bee).keeper.hasMask; // OK
// createInstance(Bee).keeper.nametag; // エラー:nametagはBeeに存在しない
// createInstance(Plant).keeper.nametag; // エラー:PlantはAnimの子クラスでない
上記はクラス継承で起きる複雑な例で
個人的にはですが、クラス継承は多用しないほうがよいと最近考えています。
そもそもクラス継承は名前空間をごっちゃにして、親クラスまでメンバのスコープを考えないといけなくなるので
クラス継承をするくらいなら、Compositeやdelegateしたほうが良い感があります。
参考:継承より合成ってなに?
Reactの公式ドキュメントにもコンポジションを使ったほうが良いと書いてありますし
参考:コンポジション vs 継承
Hookでのfunctinal componentもコンポジション化を推進していく流れなのだと思います。
ユーティリティ型(Utility Types)
TypeScriptには、一般的な型変換を容易にするいくつかのユーティリティ型が用意されています。
これらのユーティリティはグローバルに利用可能です。
参考:https://log.pocka.io/posts/typescript-builtin-type-functions/
Partial<T>
Tのすべてのプロパティがオプショナルに設定された型を構築します。
このユーティリティは、指定されたタイプのすべてのサブセットを表すタイプを返します。
つまり、型Tのすべてのプロパティを省略可能( | undefined)にした新しい型を返すMapped Typeです。
interface Todo1 {
title: string;
description: string;
}
// Partial<Todo1>はこれと同じ
// interface Todo1 {
// title?: string;
// description?: string;
// }
// Partial<Todo>で部分的に更新して値を返す
function updateTodo(todo: Todo1, fieldsToUpdate: Partial<Todo1>): Todo1 {
return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
title: "organize desk",
description: "clear clutter"
};
const todo2 = updateTodo(todo1, {
description: "throw out trash"
});
console.log(todo2);
Readonly<T>
Tのすべてのプロパティが読み取り専用に設定された型を構築します。
つまり、構築された型のプロパティは再割り当てできません。
interface Todo2 {
title: string;
}
// Readonly<Todo2>はこれと同じ
// interface Todo2 {
// readonly title: string;
// }
const todo3: Readonly<Todo2> = {
title: "Delete inactive users"
};
// todo3.title = "Hello"; // エラー:titleはreadonly
console.log(todo3);
// このユーティリティは、実行時に失敗する割り当て式を表すのに役立ちます
// (つまり、Object.freezeされたプロパティを再割り当てしようとする場合)。
declare function freeze<T>(obj: T): Readonly<T>;
Record<K,T>
型TのプロパティKのセットを持つ型を構築します。
このユーティリティは、型のプロパティを別の型にマップするために使用できます。
// マッピングする値のinterface
interface PageInfo {
title: string;
}
// マップ対象のキー
type Page = "home" | "about" | "contact";
const rec: Record<Page, PageInfo> = {
about: { title: "about" },
contact: { title: "contact" },
home: { title: "home" }
// hoge: {title: 'hoge'} // Pageにhogeは定義されていないのでマップできない
};
console.log(rec);
Pick<T,K>
TからプロパティKのセットを選択して、型を構築します。
interface Todo3 {
title: string;
description: string;
completed: boolean;
}
// Pick<Todo3, "title" | "completed">はこれと同じ
// interface Todo3 {
// title: string;
// completed: boolean;
// }
const todo4: Pick<Todo3, "title" | "completed"> = {
title: "Clean room",
completed: false
};
console.log(todo4);
Omit<T,K>
Tからすべてのプロパティを選択してKを削除することにより、型を構築します。
interface Todo4 {
title: string;
description: string;
completed: boolean;
}
// Omit<Todo4, "description">はこれと同じ
// interface Todo4 {
// title: string;
// completed: boolean;
// }
const todo5: Omit<Todo4, "description"> = {
title: "Clean room",
completed: false
};
console.log(todo5);
Exclude<T,U>
TからUに割り当て可能なすべてのプロパティを除外することにより、型を構築します。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
// const alpha1: T0 = "a"; // エラー:"a"は除外済み
const alpha2: T0 = "c";
console.log(alpha2);
// const beta1: T2 = (): void => {}; // エラー:() => voidは除外済み
const beta2: T2 = 0;
console.log(beta2);
Extract<T,U>
TからUに割り当て可能なすべてのプロパティを抽出することにより、型を構築します。
type T3 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T4 = Extract<string | number | (() => void), Function>; // () => void
const gumma1: T3 = "a";
// const gumma2: T3 = "f"; // エラー:"f"は"a" | "b" | "c"に含まれていない
console.log(gumma1);
NonNullable<T>
Tからnullおよびundefinedを除外して、型を構築します。
type T5 = NonNullable<string | number | undefined>; // string | number
type T6 = NonNullable<string[] | null | undefined>; // string[]
// let theta1: T6 = null; // エラー:null指定できない
let theta2: T6 = ["x", "y"];
console.log(theta2);
ReturnType<T>
関数Tの戻り値の型で構成される型を構築します。
function f1(): { a: number; b: string } {
return { a: 1, b: "hello" };
}
type T7 = ReturnType<() => string>; // string
type T8 = ReturnType<(s: string) => void>; // void
type T9 = ReturnType<<T>() => T>; // {}
type T10 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T11 = ReturnType<typeof f1>; // { a: number, b: string }
type T12 = ReturnType<any>; // any
type T13 = ReturnType<never>; // any
// type T14 = ReturnType<string>; // エラー
// type T15 = ReturnType<Function>; // エラー
const func1: T11 = f1();
console.log(func1);
InstanceType<T>
コンストラクター関数型Tのインスタンス型で構成される型を構築します。
class C {
public x: number = 0;
public y: number = 0;
}
type T16 = InstanceType<typeof C>; // C
type T17 = InstanceType<any>; // any
type T18 = InstanceType<never>; // any
// type T19 = InstanceType<string>; // エラー
// type T20 = InstanceType<Function>; // エラー
const cla: T16 = new C();
console.log(cla);
Required<T>
requiredに設定されたTのすべてのプロパティで構成される型を構築します。
interface Props {
a?: number;
b?: string;
}
// Required<Props>はこれと同じ
// interface Props {
// a: number;
// b: string;
// }
const obj1: Required<Props> = { a: 5, b: "ok" }; // OK
// const obj2: Required<Props> = { a: 5 }; // エラー: 'b'も必須パラメータ
console.log(obj1);
ThisType<T>
thisの型をTとすることができる特殊な型です。
このユーティリティを使用するには、-noImplicitThisフラグを有効にする必要があることに注意してください。
// 次の例では、makeObjectへの引数のメソッドオブジェクトには、ThisType <D&M>を含む型があります。
// したがって、methodsオブジェクト内のメソッドのthisの型は{x:number、y:number}&{moveBy(dx:number、dy:number):number}です
// メソッドプロパティの型が同時に推論ターゲットであり、メソッドのthisの型を決定するものであることに注意してください。
interface ObjectDescriptor<D, M> {
data: D;
methods: M & ThisType<D & M>; // methodの「this」の型はD&M
}
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: D = desc.data;
let methods: M = desc.methods;
const ret: D & M = { ...data, ...methods };
return ret;
}
const obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number): void {
this.x += dx; // 強い型付されたthis
this.y += dy; // 強い型付されたthis
// this.z = 10; // noImplicitThisを有効にしている場合はエラー:プロパティ 'z' は型 '{ x: number; y: number; } & { moveBy(dx: number, dy: number): void; }' に存在しません。
}
}
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
console.log(obj);
Parameters<T>
関数型Tの引数の型をタプルとして抽出します。
function foo1(arg1: string, arg2: number): void {}
function bar1(): void {}
type Foo1 = Parameters<typeof foo1>; // [string, number]
type Bar1 = Parameters<typeof bar1>; // []
const paramsFoo: Foo1 = ["arg1", 100];
// const paramsNotFoo: Foo = ["arg1", 100, true]; // エラー:型が一致しない
console.log(paramsFoo);
ConstructorParameters<T>
型Tのコンストラクタの引数の型をタプルとして抽出します。
Parametersのコンストラクタ版です。
class Foo2 {
public constructor(arg1: string, arg2?: boolean) {}
}
type Bar2 = ConstructorParameters<typeof Foo2>; // [string, boolean] | [string]
const constructorParams: Bar2 = ["abc", true];
console.log(constructorParams);
型の応用(Advanced types)
かなり難しい内容も含みます。
交差型(Intersection Types)
交差型は、複数の型を1つに結合します。
これにより、既存の型を追加して、必要なすべての機能を備えた単一の型を取得できます。
たとえば、Personal&LoggableはPersonalおよびLoggableのすべてのパラメータを持ちます
mixinや、従来のオブジェクト指向型に適合しない他の概念に使用される交差型が主に表示されます。
(JavaScriptにはこれらの多くがあります!)
mixinの作成方法を示す簡単な例を次に示します。
interface Personal {
name: string;
}
interface Loggable {
log(data: string): void;
}
// 交差型はPersonal & Loggable両方のパラメータを持つ
const jim: Personal & Loggable = {
name: "jim",
log: (data: string): void => {
console.log(`I'am ${data}`);
}
};
jim.log(jim.name);
共有体型(Union Types)
共有体型は交差型と密接に関連していますが、使用方法は大きく異なります。
共有体型は、いくつかの型のいずれかになりうる値を|を使用して各型を分離して記述します。
function padLeft(value: string, padding: string | number): string | number {
// typeofで元の型をチェックする(typeof型ガード)
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
console.log(padLeft("Hello world", 1)); // OK
// console.log(padLeft("Hello world", true)); // エラー:paddingがstring, number以外はコンパイルエラー
共有体型を持つ値がある場合、共有体内のすべての型に共通するメンバーにのみアクセスできます。
値のタイプがA | Bの場合、AとBの両方が持っているメンバーがいることだけを保証しています。
interface Bird {
fly: number;
layEggs: string;
}
interface Fish {
swim: number;
layEggs: string;
}
function getSmallPet(): Fish | Bird {
return { fly: 10, swim: 10, layEggs: "abc" };
}
let pet = getSmallPet();
console.log(pet.layEggs); // OK
// console.log(pet.swim); // エラー:共通メンバーでない
型ガードと区別型(Type Guards and Differentiating Types)
Fish型かBird型か2つの可能な値を区別するJavaScriptの一般的なイディオムは、メンバーの存在を確認することです。
前述したように、共有体型のすべての構成型に含まれていることが保証されているメンバーにのみアクセスできます
これらの各プロパティアクセスはエラーを引き起こします
if (pet.swim) {
console.log(pet.swim);
} else if (pet.fly) {
console.log(pet.fly);
}
同じコードを機能させるには、型アサーションを使用する必要があります。
if ((pet as Fish).swim) {
console.log((pet as Fish).swim);
} else if ((pet as Bird).fly) {
console.log((pet as Bird).fly);
}
ユーザー定義の型ガード(User-Defined Type Guards)
TypeScriptには型ガードと呼ばれるものがあります。
型ガードは、あるスコープ内のタイプを保証するランタイムチェックを実行する式です。
型述語(is)の使用
型ガードを定義するには、戻り値の型が型述語である関数を定義するだけです。
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
isFishが呼び出されると、TypeScriptは元の型に互換性がある場合、その変数をその特定の型に絞り込みます。
そのため、if文の中のpetは型アサーションする必要がなくなります(else文の方も型が特定されます)
if (isFish(pet)) {
console.log(pet.swim);
} else {
console.log(pet.fly);
}
in演算子を使用する
in演算子は、型の絞り込み式として機能するようになりました。
nが文字列リテラルまたは文字列リテラル型で、xが共有体型であるx式のnの場合、
「true」の場合は、オプションまたは必須のプロパティnを持つ型に絞り込まれます
「false」の場合は、オプションのプロパティnまたは欠落しているプロパティnを持つタイプに絞り込まれます。
function move(pet: Fish | Bird): number {
// Fish or Birdのswimプロパティを持っている場合=Fish型の場合
if ("swim" in pet) {
return pet.swim; // Fish
}
return pet.fly; // Bird
}
console.log(move(pet));
typeof型ガード
typeof演算子で型の特定をします。
function padLeft(value: string, padding: string | number): string | number {
// typeofで元の型をチェックする(typeof型ガード)
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
instanceof型ガード
classの型の特定をするためにはinstanceof演算子を使用します。
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
private numSpaces: number;
public constructor(numSpaces: number) {
this.numSpaces = numSpaces;
}
public getPaddingString(): string {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
private value: string;
public constructor(value: string) {
this.value = value;
}
public getPaddingString(): string {
return this.value;
}
}
function getRandomPadder(): SpaceRepeatingPadder | StringPadder {
return Math.random() < 0.5
? new SpaceRepeatingPadder(4)
: new StringPadder(" ");
}
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
console.log("SpaceRepeatingPadder" + padder.getPaddingString() + "."); // SpaceRepeatingPadder型に特定されている
}
if (padder instanceof StringPadder) {
console.log("StringPadder" + padder.getPaddingString() + "."); // StringPadder型に特定されている
}
型ガードと型アサーション(Type guards and type assertions)
null許容型は共有体で実装されるため、型ガードを使用してnullを取り除く必要があります。
幸いなことに、これはJavaScriptで記述するコードと同じです。
function f3(sn: string | null): string {
if (sn == null) {
return "default";
} else {
return sn;
}
}
console.log(f3(null));
ここでは、nullの除去に明白ですが、|| 演算子も使用できます。
function f4(sn: string | null): string {
return sn || "default";
}
console.log(f4(null));
コンパイラーがnullまたはundefinedを除去できない場合、型演算子を使用してそれらを手動で削除できます。
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + ". the " + epithet; // エラー:'name'がnullの可能性があります
}
name = name || "Bob";
return postfix("great");
}
// 構文は末尾!:identifier!です。識別子のタイプからnullおよびundefinedを削除します。
function fixed(name: string | null): string {
function postfix(epithet: string | null): string {
// return name!.charAt(0) + ". the " + epithet; // ok
// eslint no-non-null-assertionを入れている場合上の書き方でも怒られる
return (name || "").charAt(0) + ". the " + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
console.log(fixed(null));
型エイリアス(Type Aliases)
型エイリアスは、型の新しい名前を作成します。
型エイリアスはインターフェースに似ている場合がありますが、
プリミティブ型、共有体、タプル、その他の手作業で記述する必要のある型に名前を付けることができます。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === "string") {
return n;
} else {
return n();
}
}
console.log(getName("hoge"));
インターフェースと同様に、型エイリアスもジェネリックにすることができます。
型パラメータを追加して、エイリアス宣言の右側で使用するだけです。
interface Container<T> {
value: T;
}
プロパティで自分自身を参照する型エイリアスを持つこともできます
interface Tree<T> {
value: T;
left: Tree<T>;
right: Tree<T>;
}
交差型を併用すると、かなり心を折る型を作成できます
type LinkedList<T> = T & { next?: LinkedList<T> };
interface Human {
name: string;
}
const humans: LinkedList<Human> = {
name: "a",
next: { name: "b", next: { name: "c", next: { name: "d" } } }
};
let human = humans;
console.log(human.name);
while (human.next) {
human = human.next;
console.log(human.name);
}
ただし、型エイリアスを宣言の右式に同名のエイリアス名を表記することはできません。
type Yikes = Array<Yikes>; // エラー
インターフェース vs 型エイリアス(Interfaces vs Type Aliases)
前述したように、型エイリアスはインターフェースのように機能します。ただし、微妙な違いがいくつかあります。
違いの1つは、インターフェースがどこでも使用される新しい名前を作成することです。
型エイリアスは新しい名前を作成しません。たとえば、エラーメッセージはエイリアス名を使用しません。
interface Alias {
num: number;
}
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
Open–closed principleに基づくと、
(クラス、モジュール、関数などは拡張のために開かれるべきであるが、修正のために閉じられるべきであるという原則)
可能な場合は常に型エイリアスを介したインターフェースを使用する必要があります。
一方、インターフェースで何らかの形を表現できず、共有体型またはタプル型を使用する必要がある場合、通常は型エイリアスを使用します。
文字列リテラル型(String Literal Types)
文字列リテラル型を使用すると、文字列に必要な正確な値を指定できます。
実際には、文字列リテラル型は、共有体型、型ガード、および型エイリアスとうまく組み合わされます。
これらの機能を併用すると、文字列で列挙型の動作を得ることができます。
// 文字列リテラル
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
public animate(dx: number, dy: number, easing: Easing): void {
if (easing === "ease-in") {
// ...
} else if (easing === "ease-out") {
// ...
} else if (easing === "ease-in-out") {
// ...
} else {
// error! should not pass null or undefined.
}
}
}
let btn = new UIElement();
btn.animate(0, 0, "ease-in");
// btn.animate(0, 0, "uneasy"); // エラー: "uneasy"は文字列リテラルに定義されていない
数値リテラル(Numeric Literal Type)
TypeScriptは数値リテラルも持つことができます
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
switch (Math.round(Math.random() * 5 + 1)) {
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
case 4:
return 4;
case 5:
return 5;
case 6:
return 6;
default:
throw new Error("予期しない数値");
}
}
console.log(rollDice());
これらは明示的に記述されることはめったにないですが、型の絞り込みがバグをキャッチできる場合に役立ちます。
function foo(x: number): void {
if (x !== 1 || x !== 2) {
// エラー:This condition will always return 'true' since the types '1' and '2' have no overlap.
// つまり、xが2と比較される場合、xは1でなければなりません。これは、上記のチェックが無効な比較を行うことを意味します。
}
}
識別共有体型(Discriminated Union)
シングルトン型、共有体型、型ガード、および型エイリアスを組み合わせて、識別共有体型と呼ばれる高度なパターンを作成できます。
これは、タグ付き共有体または代数データ型とも呼ばれます。
関数型プログラミングで役立ちます。一部の言語では、Unionが自動的に区別されます。
TypeScriptは、現在存在するJavaScriptパターンに基づいて構築されます。 3つの要素があります。
1.共通のシングルトン型プロパティを持つ型 — 判別式。
2.これらの型の和集合(和集合)をとる型エイリアス。
3.共通プロパティの型ガード。
最初に、結合するインターフェースを宣言します。各インターフェースには、異なる文字列リテラルタイプのkindプロパティがあります。
kindプロパティは、判別式またはタグと呼ばれます。他のプロパティは各インターフェースに固有です。インターフェースは現在無関係であることに注意してください。
interface SquareBox {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
// 識別共有体でinterfaceを列挙する
type ShapeType = SquareBox | Rectangle | Circle;
// 識別共有体のkindに合わせて面積を計算する
function area(s: ShapeType): number {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
}
}
console.log(area({ kind: "square", size: 10 }));
console.log(area({ kind: "rectangle", width: 10, height: 5 }));
console.log(area({ kind: "circle", radius: 5 }));
網羅性チェック(Exhaustiveness checking)
識別共有体のすべてのバリエーションをカバーしていない場合、コンパイラーエラーになってほしいときがあります。
たとえば、ShapeTypeにTriangleを追加する場合、areaも更新する必要があります。
interface Triangle {
kind: "triangle";
width: number;
height: number;
}
type ShapeType = SquareBox | Rectangle | Circle | Triangle;
// --strictNullChecksが有効の場合、switch文が完全ではなくなったため、エラーになります
function area2(s: ShapeType): number {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
}
// エラーになってほしい - case "triangle"を処理してない
}
もうひとつの方法は、コンパイラが網羅性をチェックするためにnever型を使用します。
// 引数をnever型にしているのがポイント
function assertNever(x: never): never {
throw new Error("予期しないオブジェクト:" + x);
}
function area3(s: ShapeType): number {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.radius ** 2;
default:
return assertNever(s); // エラー:ここにコンパイルチェックで到達するのはcaseの網羅性忘れ
}
}
多様性this型(Polymorphic this types)
多様性this型は、包含クラスまたはインターフェースのサブタイプである型を表します。
これは、F-boundedポリモーフィズムと呼ばれます。
これにより、たとえば、階層的な流動的なインターフェースの表現がはるかに簡単になります。
各操作の後にthisを返す簡単な計算をします。
class BasicCalculator {
protected value: number;
public constructor(value: number = 0) {
this.value = value;
}
public currentValue(): number {
return this.value;
}
// 戻り値型にthisを返す(多様性this型)
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value *= operand;
return this;
}
// ... 他の操作をここに書く ...
}
const v1 = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();
console.log(v1);
// クラスはthis型を継承することができ、子クラスは変更せずに古いメソッドを使用できます。
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin(): this {
this.value = Math.sin(this.value);
return this;
}
// ... 他の操作をここに書く ...
}
let v2 = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();
console.log(v2);
インデックス型(Index types)
インデックス型を使用すると、コンパイラに動的プロパティ名を使用するコードをチェックさせることができます。
たとえば、一般的なJavascriptパターンは、次のようなオブジェクトからプロパティのサブセットを選択することです。
function pluck(o, propertyNames) {
return propertyNames.map(n => o[n]);
}
インデックス型のクエリ演算子とインデックス付きアクセス演算子を使用して、
TypeScriptでこの関数を記述して使用する方法は次のようになります。
function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
return propertyNames.map((n: K): T[K] => o[n]);
}
interface Car {
manufacturer: string;
model: string;
year: number;
}
let taxi: Car = {
manufacturer: "Toyota",
model: "Camry",
year: 2014
};
// manufacturerとmodelプロパティを取得
let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
// modelとyearプロパティを取得
let modelYear = pluck(taxi, ["model", "year"]);
console.log(makeAndModel);
console.log(modelYear);
コンパイラーは、manufacturerとmodelが実際にCarのプロパティであることを確認します。
この例では、いくつかの新しい型の演算子を紹介しています。
1つは、インデックス型のクエリ演算子であるkeyof Tです。
型Tの場合、keyof Tは、Tの既知のパブリックプロパティ名の結合です。次に例を示します。
let carProps: keyof Car; // 'manufacturer' | 'model' | 'year'の共有体型
keyof Carは'manufacturer' | 'model' | 'year'
の共有体型と完全に互換性がありますが、
違いはCarにownersAddress: stringなどの別のプロパティを追加した場合にkeyofも自動で更新されるところです。
また、事前にプロパティ名を知ることができない場合、pluckなどの一般的なコンテキストでkeyofを使用できます。
つまり、コンパイラは、pluckに正しいプロパティ名のセットを渡すことを確認します。
次のように存在していないプロパティを渡すとコンパイルエラーになります。
pluck(taxi, ['year', 'unknown']); // unknownはkeyに定義されていない
2番目の演算子は、インデックス付きアクセス演算子T[K]
です。
ここで、型の構文は式の構文を反映しています。
インデックス型のクエリ演算子と同様に、汎用コンテキストでT[K]
を使用できます。
型変数Kがkeyof Tをextendsすることを確認する必要があります。
getObjPropertyという名前の関数を使用した別の例を次に示します。
getObjPropertyでは、o:T
およびpropertyName:K
であるため、o[propertyName]:T[K]
を意味します。
T[K]
の結果を返すと、コンパイラはキーの実際の型をインスタンス化するため、
getObjPropertyの戻り値の型は、要求するプロパティによって異なります。
function getObjProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName]はT[K]型
}
インデックス型とインデックスシグネチャ(Index types and index signatures)
keyofとT[K]はインデックスシグネチャと相互作用します。
インデックスシグネチャパラメーターの型は「string」または「number」でなければなりません。
文字列インデックスシグネチャを持つ型がある場合、keyof Tは文字列になります。
JavaScriptでは、文字列(object['42'])または数字(object[42])を使用してオブジェクトプロパティにアクセスできるため、単なる文字列ではありません。
T[string]は単なるインデックスシグネチャの型です。
interface Dictionary1<T> {
[key: string] : T;
}
let keys1: keyof Dictionary1<number> = "abc"; // string | number
let value1: Dictionary1<number>["foo"] = 1; // number
console.log(keys1);
console.log(value1);
// 数値インデックスシグネチャを持つ型がある場合、keyof Tは単なる数値になります。
interface Dictionary2<T> {
[key: number] : T;
}
let keys2: keyof Dictionary2<number> = 3; // number
// let value2: Dictionary2<number>["foo"]; // エラー:プロパティ'foo'は型'Dictionary2<number>'に存在しません
let value3: Dictionary2<number>[42] = 10; // number
console.log(keys2);
console.log(value3);
マップされた型(Mapped types)
既存の型を取得し、その各プロパティをオプショナルにしたいことがあります。
interface PersonPartial {
name?: string;
age?: number;
}
// もしくはreadonlyバージョンを作りたいことがあります
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
これはJavascriptで十分に頻繁に発生するため、TypeScriptは古い型(マップされた型)に基づいて新しい型を作成する方法を提供します。
マップされた型では、新しい型が古い型の各プロパティを同じ方法で変換します。
たとえば、型のすべてのプロパティを読み取り専用またはオプショナルにすることができます。
これはユーティリティ型のReadonlyとPartialの定義です。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
そして次のように使います
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
メンバーを追加する場合は、交差型を使用できます。
この構文は、メンバーではなく型を記述することに注意してください。
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
} & { newMember: boolean };
// 次のように書いてはいけません
// これはエラーになります
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
newMember: boolean;
}
最も単純なマップされた型とその部分を見てみましょう。
type Keys = "option1" | "option2";
type Flags = { [K in Keys]: boolean };
構文は、内部にfor ..があるインデックスシグネチャの構文に似ています。
- 型変数Kは各変数に順番にバインドされます。
- 反復処理するプロパティの名前を含む、文字列リテラル共有体型のKeysを指定します。
- 値のプロパティの型を指定します。
この簡単な例では、このマップされた型は次の記述と同等です。
interface Flags {
option1: boolean;
option2: boolean;
}
既存の型に基づいており、何らかの方法でプロパティを変換します。
その場合はkeyofおよびインデックス付きアクセス型の出番です
type Nullable<T> = { [P in keyof T]: T[P] | null };
type Partial<T> = { [P in keyof T]?: T[P] };
これらの例では、プロパティリストはkeyof Tであり、結果の型はT[P]の亜種です。
これができるのは、マップされた型は一般的な使用に適したテンプレートであり、
この種の変換が準同型であるためです。
つまり、マッピングはTのプロパティにのみ適用され、他のプロパティには適用されないということです。
コンパイラは、新しいプロパティ修飾子を追加する前に、既存のプロパティ修飾子をすべてコピーできることを知っています。
たとえば、Person.nameがreadonlyの場合、Partial.nameはreadonlyでかつオプショナルです。
interface Proxy<T> {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
[P in keyof T] : Proxy<T[P]>;
};
function proxify<T>(o: T): Proxify<T> {
let result: Partial<Proxify<T>> = {};
for (const k in o) {
result[k] = {
get(): T[Extract<keyof T, string>] {
return o[k];
},
set(value): void {
o[k] = value;
}
};
}
return result as Proxify<T>;
}
let proxyProps = proxify({ a: 10, b: 20 });
proxyProps.a.set(30);
console.log(proxyProps.a.get());
ReadonlyとPartialの他に標準ライブラリのユーティリティ型にはPick、Recordがあります
type Pick<T, K extends keyof T> = {
[P in K] : T[P];
};
type Record<K extends keyof any, T> = {
[P in K] : T;
};
Readonly、Partial、Pickは準同型ですが、Recordはそうではありません。
Recordが準同型ではないことの1つの手がかりは、プロパティをコピーするために入力型を使用しないことです
type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>;
非準同型は基本的に新しいプロパティを作成するため、どこからでもプロパティ修飾子をコピーできません。
マップされた型からの推論(Inference from mapped types)
型のプロパティをラップする方法がわかったので、次に行うことは、それらのラップを解除することです。
このアンラップ推論は準同型マップ型でのみ機能することに注意してください。
マップされた型が準同型でない場合は、ラップ解除関数に明示的な型パラメーターを指定する必要があります。
function unproxify<T, P>(t: Proxify<T>): T {
let result: Partial<T> = {};
for (const k in t) {
result[k] = t[k].get();
}
return result as T;
}
let originalProps = unproxify(proxyProps);
console.log(originalProps);
条件付き型(Conditional Types)
TypeScript 2.8には、非均一型マッピングを表現する機能を実現する条件付き型が導入されています。
条件付き型は、型関係テストとして表される条件に基づいて、2つの可能な型のいずれかを選択します。
T extends U ? X : Y
上記の型は、TがUに割り当て可能な場合、型はXであり、それ以外の場合、型はYです。
条件付き型extends U ? X : Y
はXまたはYに即時評価されるか、条件が1つ以上の型変数に依存するために遅延評価されます。
TまたはUに型変数が含まれる場合、XまたはYに即時評価するか、遅延評価するかは、
型システムにTが常にUに割り当て可能であると結論付けるのに十分な情報があるかどうかによって決まります
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
type U0 = TypeName<string>; // "string"
type U1 = TypeName<"a">; // "string"
type U2 = TypeName<true>; // "boolean"
type U3 = TypeName<() => void>; // "function"
type U4 = TypeName<string[]>; // "object"
const test0: U0 = "string"; // string型であれば"string"という文字しか割り当てできない
const test1: U1 = "string"; // string型であれば"string"という文字しか割り当てできない
const test2: U2 = "boolean"; // boolean型であれば"boolean"という文字しか割り当てできない
const test3: U3 = "function"; // function型であれば"function"という文字しか割り当てできない
const test4: U4 = "object"; // object型であれば"object"という文字しか割り当てできない
console.log(test0);
console.log(test1);
console.log(test2);
console.log(test3);
console.log(test4);
条件付き型が遅延評価される例:
条件付き型関数の利用時に評価されます。
interface Foo {
propA: boolean;
propB: boolean;
}
function f<T>(x: T): T extends Foo ? string : number {
// 型分岐
if (typeof x === "object" && "propsA" in x && "propsB" in x) {
return JSON.stringify(x) as T extends Foo ? string : number;
} else {
return 0 as T extends Foo ? string : number;
}
}
function foo<U>(x: U): void {
// 外部からのxパラメータ指定時に遅延評価される
let a = f(x);
let b: string | number = a;
console.log(b);
}
foo({ propsA: true, propsB: false });
foo("piyo");
上記では、変数aには型分岐をまだ選択していない条件型があります。別のコードがfooを呼び出すことになったとき
Uで他の型に置き換えられ、TypeScriptは条件付き型を再評価して、実際に型分岐が正しいかどうかを判断します。
分配条件付き型(Distributive conditional types)
引数型が共有体型のパラメーターである条件付き型は、分散型条件付き型と呼ばれます。
分散条件付き型は、インスタンス化中に共有体型に自動的に分散されます。
例えば、A | B | C
引数型を取るT extends U ? X : Y
インスタンスのTは
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
を解決します
type U5 = TypeName<string | (() => void)>; // "string" | "function"
type U6 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined"
type U7 = TypeName<string[] | number[]>; // "object"
const test5: U5 = "string";
const test6: U6 = "undefined";
const test7: U7 = "object";
console.log(test5);
console.log(test6);
console.log(test7);
条件付き型は、マップされた型と組み合わせると特に便利です。
type FunctionPropertyNames<T> = {
[K in keyof T] : T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts?: Part[];
updatePart(newName: string): void;
}
type U8 = FunctionPropertyNames<Part>; // "updatePart"
type U9 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts"
type U10 = FunctionProperties<Part>; // { updatePart(newName: string): void }
type U11 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
const test8: U8 = "updatePart";
const test9: U9 = "name";
const test10: U10 = {
updatePart(newName: string): void {
console.log(newName);
}
};
const test11: U11 = { id: 1, name: "name" };
console.log(test8);
console.log(test9);
console.log(test10);
console.log(test11);
共有体型および交差型と同様に、条件付き型は自身を再帰的に参照することはできません。たとえば、次はエラーになります。
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // エラー
条件付き型の型推論(Type inference in conditional types)
条件付き型のextends句内で、推定される型変数を導入するinfer宣言を持つことが可能になりました。
このような推論された型変数は、条件付き型の条件分岐で参照される場合があります。
同じ型変数に対して複数の推論箇所を持つことが可能です。
たとえば、ReturnTypeユーティリティ型は関数型の戻り値の型を抽出します。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
条件型をネストして、順番に評価されるパターンマッチのシーケンスを形成できます。
type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
type V0 = Unpacked<string>; // string
type V1 = Unpacked<string[]>; // string
type V2 = Unpacked<() => string>; // string
type V3 = Unpacked<Promise<string>>; // string
type V4 = Unpacked<Promise<string>[]>; // Promise<string>
type V5 = Unpacked<Unpacked<Promise<string>[]>>; // string
const ret0: V0 = "string";
const ret1: V1 = "string";
const ret2: V2 = "string";
const ret3: V3 = "string";
// top-level-await、ES2019の文法
// https://github.com/tc39/proposal-top-level-await
const ret4: V4 = Promise.resolve("string");
const ret5: V5 = "string";
console.log(ret0);
console.log(ret1);
console.log(ret2);
console.log(ret3);
console.log(ret4);
console.log(ret5);
次の例は、inferがメソッドのリターン値として使われる場合
そのinferの型は共有体型として推測されます
type Hoge<T> = T extends { a: infer U; b: infer U } ? U : never;
type Z0 = Hoge<{ a: string; b: string }>; // string
type Z1 = Hoge<{ a: string; b: number }>; // string | number
const inf0: Z0 = "abc";
const inf1: Z1 = "def";
const inf2: Z1 = 2;
console.log(inf0);
console.log(inf1);
console.log(inf2);
同様に、inferがメソッドのパラメータ値として使われる場合、
交差型として推測されます。
type Huga<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
? U
: never;
type Z2 = Huga<{ a: (x: string) => void; b: (x: string) => void }>; // string
type Z3 = Huga<{ a: (x: string) => void; b: (x: number) => void }>; // string & number => never
const inf3: Z2 = "abc";
// const inf4: Z3 = "def" & 1; // エラー:ありえない
console.log(inf3);
複数の呼び出しシグネチャを持つ型(オーバーロードされた関数の型など)から推測する場合、最後のシグネチャから推測が行われます。
引数型のリストに基づいてオーバーロード解決を実行することはできません。
declare function piyo(x: string): number;
declare function piyo(x: number): string;
declare function piyo(x: string | number): string | number; // これが適用される
type Z4 = ReturnType<typeof piyo>; // string | number
const inf4: Z4 = "fin";
console.log(inf4);
通常の型パラメーターの制約句でinfer宣言を使用することはできません。
type ReturnType<T extends (...args: any[]) => infer R> = R; // エラー:サポートしていません
ただし、制約句の型変数を消去し、代わりに条件付きの型を指定することで、ほぼ同じ効果を得ることができます。
type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R
? R
: any;
namespaceと型定義ファイル
今まではグローバルスコープに変数やinterfaceを定義していました。
TypeScriptは全体をコンパイルするため、グローバルスコープに書いているinterfaceやtypeの名前は
ファイルをまたいで参照でき、同名のtypeやinterfaceを再定義すると重複エラーとなります。
(グローバル汚染)
この場合、namespaceを使うことでグローバルスコープを汚さないですむことができます。
また、interfaceを別ファイルで管理したくなります。
このような場合、型定義を作成すると一括で管理できるようになります。
ファイル名.d.ts
というファイルを作成します。
今回はtypesフォルダ以下にinterface.d.tsを作成しました。
参考:TypeScriptプロジェクトに独自の型定義を配置する方法
namespaceで名前空間を定義します。
export namespace
を入れ子にすることもできます。
外部から利用したいinterfaceや型定義typeはexportします。
export namespace com {
// namespaceを入れ子にできるcom.modelのようにアクセスする
export namespace model {
export interface TestModel {
title: string;
age: number;
}
}
}
tsconfig.jsonのcompilerOptionsに以下の設定を追加します。
"baseUrl": "./", /* モジュール名を解決するためのベースディレクトリ。絶対パス不可 */
"paths": {"interface": ["types/interface"]}, /* 'baseUrl'に相対的な参照位置にインポートを再マップする一連のエントリ。 */
"typeRoots": ["types", "node_modules/@types"], /* 型定義を含むフォルダのリスト。 */
利用側ではtypes/interface.d.tsをimportします。
また名前空間のエイリアスを利用すると毎回namespaceの入れ子をたどらなくて済むようになります。
import { com } from "./types/interface";
// namespaceのエイリアス
import TestModel = com.model.TestModel;
const my: TestModel = { title: "test", age: 10 };
console.log(my);