TypeScriptで型安全なScalaのcase classのcopyを作る

# 経緯

最近、TypeScriptを初めて、Reduxを動画でチュートリアルをみて、イミュータブルなデータを扱っていく感じがしたので、「これはScalaの`copy`が必要だな！」と思いました。
`copy`の必要性を感じた理由は、破壊的な変更をする思想ではないので、オブジェクトの一部を変更したいときに、変更後を返すのが自然だと思ったからです。

ところが、TypeScriptにScalaのcase classにある`copy`に相当するものが見つけられませんでした...
ということで、`copy`に相当するものを作ることにしました。


# Scalaのcopyってなんこと？ 

（**Scalaを触ったことある方は読み飛ばしてください**）

Scala以外だとHaskellでしか、これと似た機能を持った言語を知らないので、代表して、Scalaの`copy`と言ってます。他の言語にもあることを知っている方は「OOO言語のxxxね」って思ってください。

`copy`の目的は、「**破壊的に変更せずに、変更後の値を返す**」こと　です。

## 具体的な例

```scala
// 人クラスの宣言
case class Person(name: String, age: Int)
 
object Main extends App{
	// person1をインスタンス化
	val person1 = Person("jack", 2)
	// person1.ageを85に変更をしたものを作成
	val person2 = person1.copy(age=85)
 
	println(person1)
	println(person2)
}
```


## 出力
```
Person(jack,2)
Person(jack,85)
```

`person1`を破壊的に変更するという発想ではなく、`person1`部分変更が`person2`になっていることがわかります。



### 余談

「定義もしてないのに、なんで`copy`使えるの？」と思った方に。
Scalaはcase classにすると、便利なメソッドを自動で適切に定義してくれる言語になってます。`copy`の他にもいくつかの便利な関数を**適切**に定義してくれます。


# 目標

* 型安全
* 簡単に導入できる

型安全と言うのは、「型システムを使って、やってはいけないことをコンパイル時に摘出する」という意味で書いてます。


### 具体的には？
「やってはいけないこと」という意味は、Scalaで言えば、以下のようなことです。
`age`は`Int`型なので、`String`コンパイル時にエラーで間違いを教えてくれます。

```scala
// compile error
person1.copy(age="this is a string")
```


また、以下のように存在しないメンバを変更しようとした時もコンパイルエラーが欲しいですね。

```scala
// compile error
person1.copy(somethingElse=99)
```


# `interface`を使った実現方法


`...`というSpread Operatorというものを利用していると思います。
特に特別なことも必要なくとてもシンプルです。

```ts
interface IPerson{
  name: string;
  age : number;
}

const person1: IPerson = {name: "jack", age: 2};
const person2: IPerson = {...person1, age: 85};
console.log(person1)
console.log(person2);
```

参考： https://github.com/Microsoft/TypeScript/issues/7437

## コンソール出力

![Screen Shot 2017-08-07 at 23.20.09.png](https://qiita-image-store.s3.amazonaws.com/0/84451/53563fd0-caac-5ff7-f3dc-d82f8a08fddb.png)

Playgroundでも動きました。https://www.typescriptlang.org/play/

### 本当に型安全？

以下はコンパイルエラーで弾けます！

* `age`の型違い

```ts
// compile error
const person2: IPerson = {...person1, age: "this is a string"};
```

* ベースにするオブジェクトの違い

```ts
// compile error
const wrongPerson1 = {firstName: "Jack", lastName: "Sparrow"}
const person2: IPerson = {...wrongPerson1, age: 85};
```

### コンパイルエラーで弾けないもの

残念ながら、これはコンパイルエラーで弾けませんでした。

```ts
const person2: IPerson = {...person1, somethingElse: 85};
```

出力をみると、たしかに`somethingElse`が入ってしまっています...
![Screen Shot 2017-08-07 at 23.38.43.png](https://qiita-image-store.s3.amazonaws.com/0/84451/0a52755d-0a28-8477-13fa-80b3937749cd.png)

（次が本題の`copy`メソッドを作る話で、そこではこの問題は解消できます）

---
追記（2019/05/14）
現在、TypeScript 3.4.5で試したところ、ちゃんと知らないプロパティあることが、コンパイル時に弾けることが分かりました。（型システムが変わったのか、オプションなどによるものかは未調査です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/d1428049-d486-6005-a597-5076f1b932d0.png)
---

# `Copyable`クラスを作る！

以下が`Copyable`クラスの全部です。

```ts
// (from: http://blog.yux3.net/entry/2017/02/08/033834)
type Constructable<T> = new(...args: any[]) => T;

class Copyable<T>{
  constructor(private _constructor: Constructable<T>){
  }

  copy(partial: Partial<T>): T{
    const cloneObj: T = new this._constructor();　// (from: https://stackoverflow.com/a/17383858/2885946)
    return Object.assign(cloneObj, this, partial);
  }
}
```

（`from`コメントはソースコード中の参考にしたサイトたちです）

## `Copyable`の使用例


```ts
class Person extends Copyable<Person>{
  constructor(public name: string, public age: number){
    super(Person);
  }
}

const person1 = new Person("jack", 2);
const person2 = person1.copy({ age: 85 });

console.log(person1);
console.log(person2);
```

こちらのコードもPlaygroundでも動きました。https://www.typescriptlang.org/play/

注意） `Copyable`は自作クラスです。実行するときは上の`Copyable`の定義もコピペしてください。

注意） `public`はプロパティの明示的な代入をするの省くためにやってます。`public`の代わりに`readonly`でもOKです。


## コンソール出力

![Screen Shot 2017-08-07 at 23.29.26.png](https://qiita-image-store.s3.amazonaws.com/0/84451/4a5f007f-a25c-6d86-05e8-9cf16ecd1394.png)

## 本当に型安全？

以下のことがコンパイルエラーとして摘出できました。

`age`の型違い

```ts
// compile error
const person2 = person1.copy({ age: "this is a string" });
```

存在しないプロパティを変更しようとしたとき

```ts
// compile error
const person2 = person1.copy({ somethingElse: 99});
```

`super`に渡すコンストラクタな関数（？）を間違えたとき

```ts
class WrongPerson{}
class Person extends Copyable<Person>{
  constructor(public name: string, public age: number){
    super(WrongPerson); // <= compiler error
  }
}
```

## `Copyable`の使い方

以下の`A`クラスに`copy`を生やしたいときは、

```ts
class A{
  constructor(public a: number, public b: Boolean, public c: string) {
    
  }
}
```

=>

`extends Copyable<A>`と`super(A);`を追加すればOKです。

```ts
class A extends Copyable<A>{
  constructor(public a: number, public b: Boolean, public c: string) {
    super(A);
  }
}
```

### 注意点

`public`はプロパティの代入をするの省くためにやってます。`public`の代わりに`readonly`でもOKです。
実際は`readonly`のほうがimmutableなデータ構造にしたいときは、マッチしていると思いますしね。

## `Copyable`を実装するにあったて

おそらく、`Partial<T>`っていうのが、`Copyable`の型安全性にとても寄与してくれてると思います。意味は「部分的なプロパティと値の集まり」みたいな感じだと思います。

`Partial`は以下のブロクで知りました。以下のブログさんでは他に詳しく記述が書いてあって、
`type Constructable<T>`もこちらのブログさんの違う記事で見つけたものを拝借させていただきましたm(_ _)m

* http://yux3.hatenablog.com/entry/2017/02/08/033834


## 追記

前の値が欲しくなるときがあると思いそれを満たすために`Copyable`を拡張しました。

変更点は
* `PartialMap<T>`という自作の型の定義
* 新しく`mapCopy`という名前のメソッド


以下は`mapCopy`追加後の`Copyable`クラスです。

```ts

type PartialMap<T> = {
    [P in keyof T]?: (prev: T[P]) => T[P];
};

type Constructable<T> = new(...args: any[]) => T;

class Copyable<T>{
  constructor(private _constructor: Constructable<T>){
  }

  copy(partial: Partial<T>): T{
    const cloneObj: T = new this._constructor();　// (from: https://stackoverflow.com/a/17383858/2885946)
    return Object.assign(cloneObj, this, partial);
  }


  mapCopy(partial: PartialMap<T>): T {
    const cloneObj: T = new this._constructor();　// (from: https://stackoverflow.com/a/17383858/2885946)
    for (const key of Object.keys(this)) {
      if (key in partial) {
        cloneObj[key] = (partial as any)[key](this[key]);
      } else {
        cloneObj[key] = this[key];
      }
    }
    return cloneObj;
  }
}
```

### `mapCopy`の使い方

```ts
const person1 = new Person("jack", 2);
const person3 = person1.mapCopy({age: prev => prev +1});

console.log(person1);
console.log(person3);
```

### コンソール出力

<img width="327" alt="Screen Shot 2017-08-08 at 1.00.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/84451/8704b3fd-0ec4-a3ed-96b7-aebb7aed8426.png">

### `mapCopy`の型安全性

以下のことをするとちゃんとコンパイルエラーしてくれます

ageの変更後が文字列になるとき

```ts
// compile error
const person3 = person1.mapCopy({age: prev => prev + "str"});
```

存在しないプロパティのとき

```ts
// compile error
const person3 = person1.mapCopy({somethingElse: prev => prev + 1});
```

## 追記

ライブラリ化しました

https://www.npmjs.com/package/ts-copyable

```bash
npm install --save ts-copyable
```