TypeScriptハンズオン 学習メモ 4.オブジェクトをマスターする

# オブジェクトの基本

## 配列・タプルからオブジェクトへ

- 配列・タプル・・・インデックスで取り出す
- オブジェクト・・・名前で取り出す
- プロパティ・・・オブジェクトの中に保管されている値
- メソッド・・・オブジェクトの中に保管されている関数
- オブジェクトの生成

```
{ プロパティ1:値1, プロパティ2:値2, プロパティ3:値3,  ・・・}
```

```ts
const person = {
    name: "taro",
    age: 30,
    print: function():void {
        console.log(`${this.name} (${this.age})`)
    }
}

person.print()  // "taro (30)"
person.name = "hanako"
person.age = 20
person.print()  // "hanako (20)"
```


## Objectによるオブジェクト生成

- `Object()` でオブジェクトを生成し、そこに代入することでオブジェクトの必要なものを作成する方法もある。

```ts
const personObj = Object()
personObj.name = "hanako"
personObj.age = 20
personObj.print = function (): void {
    console.log(`${this.name} (${this.age})`)
}
personObj.print() // "hanako (20)"
```

## オブジェクトを生成するファクトリ関数

- 戻り値 `{ name: string, age: number, print: () => void }`
- 戻り値の指定が複雑な場合は省略してもよい

```ts
function Person(name: string, age: number): { name: string, age: number, print: () => void } {
    return {
        name: name,
        age: age,
        print: function (): void {
            console.log(`${this.name} (${this.age})`)
        }
    }
}

const person = Person("ichiro", 35)
person.print()  // "ichiro (35)" 
```

## オブジェクトを引数に使う

- objの引数は複製を渡すのではなく、オブジェクトそのものを渡すので、書き換えられる
- オブジェクトを含む変数全般は**参照が渡される**

```ts
type person = { name: string, age: number }

// objの引数は複製を渡すのではなく、オブジェクトそのものを渡すので、書き換えられる
function setData(obj: person, n: string, a: number): person {
    obj.name = n
    obj.age = a
    return obj
}

const obj1: person = { name: "taro", age: 39 }
const obj2: person = setData(obj1, "hanako", 29)

console.log(obj1) // "name": "hanako",  "age": 29
console.log(obj2) // "name": "hanako",  "age": 29
console.log(obj1 === obj2) // true
```

## オブジェクトの分割代入

- `const {name:{first, second}, age} = obj1`のように同じ構造で、プロパティと同名の変数で取り出せる（不要なプロパティは省略可）

```ts
type person = { name: { first: string, second: string }, age: number }

const obj1:person = { name:{first:"taro", second:"yamada"}, age:39}

// obj1を取り出す　（不要なプロパティは省略可）
const {name:{first, second}, age} = obj1
console.log(`${first}-${second}::${age}`)

// obj1.name.firstのみ取り出す（変数firstが使用済みのためaで取り出す）
const {name:{first:a}} = obj1
console.log(`${a}`)
```

## プロパティのオプションとReadonly

- `readonly`は書き換え不可のプロパティ
- `?`はnull許容（オブジェクト生成時に省略可能）のプロパティ

```ts
// readonlyは書き換え不可、?はnull許容（省略可）
type person = { readonly name: string, mail?: string, age?: number, print: () => void }

const obj1: person = {
    name: "taro",
    // mail,ageは省略可能
    print: function (): void { console.log(this.name) }
}

obj1.name = "jiro" // readonlyに代入しようとするとエラー
obj1.print()

const obj2: person = {
    // nameを省略するとエラー
    mail: "hanako@example.com",
    age: 29,
    print: function (): void { console.log(`${this.name}  ${this.mail}  ${this.age}` ) }
}

obj2.print()
```

# クラスの利用

## クラスの定義と利用

- `インスタンス instanceof クラス` ・・・インスタンスかどうか調べる
- `インスタンス.constructor.name` ・・・インスタンスのクラス名を得る
- `クラス.name` ・・・クラスの名前を得る

```ts
// クラスの定義
class Person {
    name:string = "no-name"
    mail:string
    age:number

    constructor(name:string, mail:string = "no-mail", age:number = -1) {
        this.name = name
        this.mail = mail
        this.age = age
    }

    print():void {
        console.log(`${this.name}(${this.mail},${this.age})`)
    }
}
// クラスの利用
const taro = new Person("taro", "taro@example.com", 39)
taro.print() // "taro(taro@example.com,39)"

// Personクラスかどうか？
console.log(taro instanceof Person) // true 
// インスタンスのクラス名は？
console.log(taro.constructor.name) // "Person" 
// クラスの名前は？
console.log(Person.name) // "Person" 
```

## クラスの継承

- `クラス名 extends 親クラス名  {  クラス定義  }`
- コンストラクタ内で、`super()`を呼び出すことにより親クラスのコンストラクタを呼ぶ
- スーパークラスのメソッドを同名のメソッドで、**オーバーライド**することができる

## アクセス修飾子について

- public（または何もつけない）・・・外部から自由にアクセスできる
- protected・・・クラス及び継承したサブクラスからアクセスできる
- private・・・クラス内のみ

## setterとgetter

```ts
class Person {
    constructor(private _name:string, private _mail:string = "no-mail", private _age:number = -1) {

    }
    print():void {
        console.log(`${this._name}(${this.mail},${this.age})`)
    }

    get name():string {
        return this._name
    }
    get mail():string {
        return this._mail
    }
    set mail(mail:string) {
        this._mail = mail
    }
    get age():number {
        return this._age
    }
}
```

## enum

- 基本・・・上から順に0から始まる数値が割り振られる

```ts
enum School {
  junior,
  juniorHigh,
  high,
  other
}

console.log(School.juniorHigh) // 1
```

- テキストを得るenum

```ts
enum School {
    junior = "junior",
    juniorHigh = "juniorHigh",
    high = "high",
    other = "other"
}

console.log(School.juniorHigh) // "juniorHigh" 
```

# クラスをさらに掘り下げる

## インタフェース

- インタフェース

```ts
// インタフェースの定義
interface Human {
    name:string
    print():void
}

// インターフェースの実装
class Person implements Human {
    name:string

    constructor(name:string) {
        this.name = name
    }
    print():void {
        console.log(this.name)
    }
}
// Human型に、Personのインスタンスを入れる
const taro:Human = new Person("taro")
taro.print()
```

## インタフェースの継承

- `インタフェース名 extends 親インタース名  {  インタフェース定義  }`
- クラスの実装時には、親インタフェースを含めたすべてのプロパティやメソッドを実装する必要がある。

## 抽象クラス

- 抽象クラスを継承したサブクラスで抽象メソッドを実装する必要がある。
- 実装したクラスのコンストラクタでは、`super()`を呼ばないとエラー。

```ts
abstract class Human {
    abstract print():void
}

class Person extends Human {
    name:string

    constructor(name:string) {
        super() // superを入れないとエラー
        this.name = name
    }

    // print()を実装しないとエラー
    print() {
        console.log(this.name)
    }
}
const taro:Human = new Person("taro")
taro.print()  // "taro"
```

### 抽象クラスとインタフェースの違い

- 他にクラスを継承する必要があるか？
    - extendsで継承できるのは1つのクラスのみであるため、抽象クラスと他のクラスを同時に継承はできない。
- プロパティを義務付ける必要があるか？
    - 抽象クラスはメソッドを定義するものであり、抽象プロパティというのはない。抽象クラスには普通のプロパティは用意できる。
- protectedメソッドか、publicメソッドか？
    - インタフェースは基本的にpublicメソッドを定義するもの。抽象クラスはprotectedメソッドは使えるが、インタフェースでは使えない。

## 静的メンバーについて

- `static プロパティ:型`
- `static メソッド(引数):型`
-  インスタンスが必要ない。
-  `name`という名前のstaticプロパティは作成できない。予約語であるため。

```ts
class Human {
    static leg:number = 2

    constructor(private _name:string, private _age:number) {
    }

    print():void {
        // staticが付かないメソッドから、staticプロパティはアクセスできる
        console.log(`name:${this._name} age:${this._age}  leg:${Human.leg}`)
    }

    static printStatic():void {
        // staticメソッドから、staticが付かないプロパティはアクセスできない
        console.log(`name:${this._name} age:${this._age}  leg:${Human.leg}`)
    }
}
const taro = new Human("taro" , 33)
taro.print() // "name:taro age:33  leg:2"

Human.printStatic() // "name:undefined age:undefined  leg:2" 
```

## パラメータプロパティ（readonlyのプロパティ）

```ts
class Human {

    constructor(readonly name:string, readonly age:number) {
    }

    set setName(name:string) {
        this.name = name // readonlyのものを変更しようとするとエラー
    }

    print():void {
        console.log(`name:${this.name} age:${this.age}`)
    }

}
const hanako = new Human("hanako" , 24)
hanako.print() // "name:hanako age:24"
```

## 総称型（ジェネリクス）の利用

- `class 名前<T> { クラス定義 }`
- 型が無いわけではなく、**実行時に型が確定する特殊な型指定**

```ts
class Data<T> {
    data: T[]

    constructor(...items: T[]) {
        this.data = items
    }

    print(): void {

        if (this.data) {
            for (const item of this.data) {
                console.log(item)
            }

        } else {
            console.log('no data...')
        }
    }
}
const data1 = new Data(10, 20, 30, 40, 100)
data1.print()
const data2 = new Data("cat", "dog", "bird", "fish")
data2.print()
const data3 = new Data(true, false, false, true)
data3.print()
```

## 総称型に使われるユーティリティ型

- `Readonly<T>`・・・変更不可にするもの
- `Required<T>`・・・必須項目にするもの（?を全て無効にする）
- `Partial<T>`・・・すべてオプションにするもの（すべて?にする）

```ts
type Human = {
    name: string,
    mail?: string,
    age?: number
}

class Person {
    // mail?やage?の?が無効になる
    human: Required<Human>

    constructor(n: string, m: string, a: number) {
        this.human = { name: n, mail: m, age: a }
    }

    // エラー（Requiredを外せばエラーとならない）
    // constructor(n: string) {
    //     this.human = {name:n}
    // }
}
```

# メモアプリを作ろう

- `npm run serve` で実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88686/15d867d1-a15a-2096-a5bb-16f7d6f30d8e.png)


- html

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Sample</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" />
    <script src="main.js"></script>
  </head>
  <body>
    <h1 class="bg-primary text-white p-2">Memo page</h1>
    <div class="container">
      <p class="h5">type message:</p>
      <div class="alert alert-primary">
        <input type="text" class="form-control" id="message" />
        <button class="btn btn-primary mt-2" id="btn">Save</button>
      </div>
      <table class="table" id="table"></table>
      <div class="text-center">
        <button class="btn btn-danger mt-4" id="initial">Initialize</button>
      </div>
    </div>
  </body>
</html>
```

- ts

```ts:index.ts
let table: HTMLTableElement;
let message: HTMLInputElement;

// htmlを画面のtableに表示する
function showTable(html: string) {
  table.innerHTML = html;
}

/**
 * Saveボタンを押下時の動き
 */
function doAction() {
  // テキストボックスからメモを取り出す
  const msg = message.value;
  // memoDataのオブジェクトに加える
  memo.add({ message: msg, date: new Date() });
  // memoData#save()を実行
  memo.save();
  // memoData#load()を実行
  memo.load();
  // memo.getHtml()の実行結果を画面に出力する
  showTable(memo.getHtml());
}

function doInitial() {
  // データを空にする
  memo.data = [];
  // 空のデータを保存
  memo.save();
  memo.load();
  // テキストボックスを空にする
  message.value = "";
  showTable(memo.getHtml());
}

type Memo = {
  message: string;
  date: Date;
};

class MemoData {
  data: Memo[] = [];

  add(mm: Memo): void {
    // 配列の先頭に追加
    this.data.unshift(mm);
  }

  save(): void {
    // localStorageに保存
    // JSON.stringifyでJSON化する
    localStorage.setItem("memo_data", JSON.stringify(this.data));
  }
  load(): void {
    // localStorageから取り出す
    // JSON.parseでオブジェクトにする
    const readed = JSON.parse(localStorage.getItem("memo_data"));
    // データが無ければ空の配列
    this.data = readed ? readed : [];
  }

  getHtml(): string {
    let html = "<thead><th>memo</th><th>date</th></thead><tbody>";
    // dataから繰り返しながら取り出す
    for (let item of this.data) {
      html += "<tr><td>" + item.message + "</td><td>" + item.date.toLocaleString() + "</td></tr>";
    }
    return html + "</tbody>";
  }
}

// ロード時の処理
const memo = new MemoData();
window.addEventListener("load", () => {
  // 画面のtable
  table = document.querySelector("#table");
  // 画面のinput
  message = document.querySelector("#message");
  // saveボタンクリック時のイベントを設定
  document.querySelector("#btn").addEventListener("click", doAction);
  // initialボタンクリック時のイベントを設定
  document.querySelector("#initial").addEventListener("click", doInitial);
  // データを取り出す
  memo.load();
  // データを画面表示する
  showTable(memo.getHtml());
});
```