TypeScript 勉強まとめ

最近 TypeScript の勉強をしているので、学んだことメモ

# TypeScript とは
* JavaScript を拡張した言語
* 最終的に JavaScript にコンパイルして使用される
* 型を持つため、潜在的なエラーを把握することができる
* JavaScript にない新しい機能を含む
    * インターフェース
    * ジェネリクス
    * デコレータ

### インストール

* node.js をインストール

    * node.js の公式サイトからインストーラを入手し、インストール

* npm で TypeScript をインストール

```bash
npm install -g typescript
```

# コンパイラ
コンパイルする ts ファイルを指定して、tsc コマンドを実行する

```bash
tsc app.ts
```


### ウォッチモード

ts ファイルを変更すると、自動でコンパイルが行われるようになる

```bash
tsc app.ts --w
```


### プロジェクト全体のコンパイル

まず、プロジェクトフォルダであることを指定する

```bash
cd [プロジェクトのルートフォルダ]
tsc --init
```

tsconfig.json ファイルが作成される
tsc コマンドを実行すると、プロジェクト内のすべての ts ファイルがコンパイルされる

```bash
tsc
```


### コンパイル する/しない ファイルを指定する

tsconfig.json に include/exclude を指定し、コンパイラの対処に する/しない ファイルを指定する

* include
    * コンパイルするファイル
    * 指定しなければ、すべての ts ファイルをコンパイルする
* exclude
    * コンパイルしないファイル
    * フォルダを指定することも可能
    * node_modules フォルダは、デフォルトでコンパイルしないようになっている


# 型定義

```typescript
const number1 = 5;    // この場合、number 型であることが明確なので型指定は不要（推論）

let number2: number;  // この場合、型がわからいないので要指定
number2 = 'aa';       // エラーになる

let number3;          // 型を指定しないと、any 型になる
number3 = 'bb';       // any 型なのでエラーにならない
```

### 引数

```typescript
function add(num1: number, num2: number, isShow: boolean) {
  const result = num1 + num2;
  if (isShow) {
    console.log(result);
  }
  return result;
}
```

### Object

```typescript
const book = {
  title: "aaa",
  page: 200,
}
```

上記のようなオブジェクトの場合、下記のように型が定義される

```typescript
{
  title: string;
  page: number;
}
```

明示的に型を指定する場合は以下のように書く（ただし、推論が可能な場合は推奨されない）

```typescript
const book: {
  title: string;
  page: number;
} = {
  title: 'aaa',
  page: 200,
}
```

### Array

```typescript
let bookshelf: string[];  // string 型の Array と指定
bookshelf= ["aaa", 1];    // string じゃないので「1」のところでエラー
```

### Tuple ： 長さが指定された配列。JavaScript にはない

```typescript
let book: [number, string] = [2, "author"];

book[1] = 10;             // string じゃないのでエラー
book = [0, 'bbb', 'ccc']; // 長さが一致しないのでエラー
```

### Enum ： 列挙型。JavaScript にはない

```typescript
enum Genre {
  BUSINESS,
  COMIC,
  NOVEL,
}

const book = {
  // ~
  genre: Genre.COMIC,
};
```

### Any ： 型をチェックしない。JavaScript にはない

```typescript
let bookshelf: any[];   // 配列であれば、中身はどんな型でもOK
bookshelf= ["aaa", 1];  // エラーにならない
```

any は極力使わないようにすべき

### Union ： 柔軟な型定義

```typescript
// input1, input2 は number型か string型を受け取れる
function add(input1: number | string, input2: number | string) {
    // ~
}
```

### Literal ： 値そのものを定義する

```typescript
// resultType には、"as-number" か "as-text" のみ指定可能
function add(input1: number, input2: number, resultType : "as-number" | "as-text") {
  // ~
}
```

### エイリアス（type） ： Union 型や Literal 型を別の名前で定義する

```typescript
type InputType = number | string;
type ResultDescriptor = "as-number" | "as-text";

function add(input1: InputType, input2: InputType, resultType : ResultDescriptor) {
    // ~
}
```

### function、void

```typescript
// return の型を指定する場合（推論される場合は記述不要）
function add(n1: number, n2: number): number {
  return n1 + n2;
}

// return がない場合 void 型になる
function printNum(num: number): void {
  console.log("Result: " + num);
}

// function の型を指定（2つの number の引数、number の戻り値）
let addValues: (a: number, b: number) => number;
addValues = add;       // OK
addValues = printNum;  // 関数の引数、戻り値の型が異なるのでエラー

```

### unknown ： どの型が入るかわからない

```typescript
let unknownInput: unknown;
let strInput: string;

unknownInput= 5;
unknownInput= "aaa";

strInput= unknownInput;  // unknownInput が string と保証されないのでエラー

// unknown を代入する場合は、型チェックが必要
if (typeof unknownInput=== "string") { 
  strInput= unknownInput;
}
```

unknownInput を any型 とするとエラーにならない（any は型チェックをしないため）

### never ： 何も返さない関数の戻り値

```typescript
function generateError(message: string, code: number): never {
  throw { message: message, errorCode: code };
}

generateError("エラーが発生しました", 500);
```

戻り値がない場合 void でもいいが、戻り値は絶対にありえないということを明示的に示す場合に never を使う

### 型キャスト

TypeScript が型を取得できない場合に、開発者が明示的に指定する方法

* 方法1　：　<型名> 式
* 方法2　：　式 as 型名

```javascript
// 下式のどちらか
const element1 = <HTMLInputElement>(document.getElementById("id1")!);
const element2 = document.getElementById("id2")! as HTMLInputElement;
```


# インターフェース

* オブジェクトがどんな形かを定義するもの
* 具体的な値を設定することはできない
* オブジェクトの型として使用でき、構造が合っているかチェックできる

```javascript
interface MsgInterface {
  name: string;
  showMessage(phrase: string): void;
}

// interface の実装クラス
class Message implements MsgInterface {
  constructor(public name: string) {}

  showMessage(phrase: string) {
    console.log(phrase + " " + this.name);
  }
}

const msg1 = new Message("World");
msg1.showMessage("Hello");
```


### インターフェースは継承も可能

```javascript
interface Named {
  readonly name: string;
}

interface MsgInterface extends Named {
  showMessage(phrase: string): void;
}
```

### 関数の型の定義

```javascript
interface AddInterface {
  (a: number, b: number): number;
}

let add: AddInterface;

add = (n1: number, n2: number) => {
  return n1 + n2;
};
```

### 任意のプロパティ

必須でないプロパティには「?」を付けることでオプションにできる

```javascript
interface Named {
  readonly name: string;
  outputName?: string;  // outputName はオプション
}
```


# ジェネリクス

### ジェネリクス関数

関数の定義時には引数を明確にしておらず、関数の使用時に引数を明確する

```javascript
// Generic 関数
function merge<T, U>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "aaa" }, { age: 30 });
```


### ジェネリック型に制約を付ける

`T extends ***` で型に制限を付ける

```typescript
// Generic 関数（引数をobjectで制限）
function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "aaa", hobbies: ["sports"] }, { age: 30 });
```

### keyof 制約

引数の一つが、別のオブジェクト型引数のキーになっていることを制約する

```typescript
function getValue<T extends object, U extends keyof T>(
  obj: T,
  key: U
) {
  return "Value: " + obj[key];
}

getValue({ name: "Max" }, "name");
```


### ジェネリック型のユーティリティ

* Partial
  * ジェネリックを一時的にオプションにする

* readonly
  * オブジェクトのプロパティの追加や変更、配列の push や pop が使えない 



### ジェネリック型と Union 型の使い分け

* Generic
  * クラス、関数の最初に型を一つに制限することができる
* Union
  * Union に指定されている、いずれかの型を常に受け入れることができる


# デコレータ

事前準備

* tsconfig.json で `"experimentalDecorators": true` のコメントを外す



### 基本的な Decorator

* 関数をクラスに適用させる

* クラスを定義した時に実行される（インスタンス化したときではない）　→　クラスの初期設定に関わる処理に使える

```typescript
function Logger(constructor: Function) {
  console.log("ログ出力中...");
  console.log(constructor);
}

@Logger
class Person {
  name = "aaa";

  constructor() {
    console.log("Person オブジェクトを作成中...");
  }
}
```



### Decorator ファクトリー

Decorator 関数に引数を渡す

```typescript
function Logger(logString: string) {
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

@Logger("ログ出力中...")
class Person {
  name = "aaa";

  constructor() {
    console.log("Person オブジェクトを作成中...");
  }
}
```

1つのクラスに複数ので Decorator を付けることが可能

* Decorator の作成は上から順に実行
* Decorator は下から順に実行

```typescript
function Logger(logString: string) {
  console.log("Loggerファクトリー");
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: string) {
  console.log("Templateファクトリー");
  return function (_: Function) {
    console.log("テンプレートを表示");
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

@Logger("ログ出力中")
@WithTemplate("<h1>Person オブジェクト<h1>", "app")
class Person {
  name = "Max";

  constructor() {
    console.log("Person オブジェクトを作成中...");
  }
}

// --- ブラウザの Console の出力 ---
// Loggerファクトリー
// Templateファクトリー
// テンプレートを表示
// ログ出力中
```



### Decorator を配置できる場所

* クラス
* プロパティ
* メソッド
* パラメータ

```typescript
function Log(target: any, propertyName: string | Symbol) {
  console.log("Property Decorator");
  console.log(target, propertyName);
}

function Log2(target: any, name: string, descripter: PropertyDescriptor) {
  console.log("Accessor Decorator");
  console.log(target);
  console.log(name);
  console.log(descripter);
}

function Log3(
  target: any,
  name: string | Symbol,
  descripter: PropertyDescriptor
) {
  console.log("Mehotd Decorator");
  console.log(target);
  console.log(name);
  console.log(descripter);
}

function Log4(target: any, name: string | Symbol, position: number) {
  console.log("Parameter Decorator");
  console.log(target);
  console.log(name);
  console.log(position);
}

class Product {
  @Log
  title: string;
  private _price: number;

  @Log2
  set price(val: number) {
    if (val > 0) {
      this._price = val;
    } else {
      throw new Error("不正な価格です - 0 以下は設定できません");
    }
  }

  constructor(t: string, p: number) {
    this.title = t;
    this._price = p;
  }

  @Log3
  getPriceWithTax(@Log4 tax: number) {
    return this._price * (1 + tax);
  }
}
```