バックエンドエンジニアがTypeScriptの型推論と戦った話

# はじめに
こんにちは。
私は普段バックエンドシステムに携わっておりフロントエンドの方の技術には疎いのですが、研修用プロジェクト的な立ち位置の社内システム開発でフロントエンドを触らせてもらえる機会があったので挑戦してみました。
初めてのことが多く、いろいろなところで詰まっては乗り越えてを繰り返しているのですが、その中でなぜか特に印象に残った内容を話します。

# 環境
Nuxt + TypeScriptで開発しています。
- Nuxt.js: `2.15.8`
- Vue.js: `2.7.10`
- TypeScript: `4.2`

# 発生した問題
状態とロジックをもつクラス(`States`)のインスタンスを、プロバイダパターンを使ってコンポーネント間で共有する という方針で、クラス定義を作成していました。
以下、とても簡略化した例です。

``` States.ts
import {reactive} from "@nuxtjs/composition-api";

export class States {
  targetDate = reactive(new Date())

  someFunction = () => {
    this.someDateFunction(this.targetDate)
  }

  someDateFunction = (date: Date) => {
    console.log(date)
  }
}
```

これをビルドしようとすると、以下のようなエラーが出てきました。

``` エラー文
TS2345: Argument of type '{ toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 38 more ...; toJSON: (key?: any) => string; }' is not assignable to parameter of type 'Date'.
  Property '[Symbol.toPrimitive]' is missing in type '{ toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 38 more ...; toJSON: (key?: any) => string; }' but required in type 'Date'.
     5 |
     6 |   someFunction1 = () => {
  >  7 |     this.someFunction2(this.date)
       |                        ^^^^^^^^^
     8 |   }
     9 |
    10 |   someFunction2 = (date: Date) => {
```

どうやらreactive化した変数`date`が、Date型ではなく、「toString()や toDateString()、 toTimeString()...etc. といった関数をもつ何かしらの型」として認識されているようです。

また、以下のようなオブジェクト型を作成して同様のコードを書いてもエラーにはなりませんでした。
```typescript
export class User {
  public name: string 

  constructor(init: Partial<User> | undefined = undefined) {
    this.name = init?.name || "Taro"
  }

  nameFunction = (): string => {
    return this.name
  }
}
```

一方、このUserにDate型のプロパティを追加すると、同様のエラーが出てくるようになります。

```typescript
export class User {
  public name: string
  public birthday: Date

  constructor(init: Partial<User> | undefined = undefined) {
    this.name = init?.name || "Taro"
    this.birthday = init?.birthday || new Date()
  }

  nameFunction = (): string => {
    return this.name
  }
}
```

``` エラー文
TS2345: Argument of type '{ name: string; birthday: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 38 more ...; toJSON: (key?: any) => string; }; nameFunction: () => string; }' is not assignable to parameter of type 'User'.
  Types of property 'birthday' are incompatible.
    Type '{ toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 38 more ...; toJSON: (key?: any) => string; }' is not assignable to type 'Date'.
     7 |   someFunction = () => {
     8 |     this.someDateFunction(this.targetDate)
  >  9 |     this.someUserFunction(this.user)
       |                           ^^^^^^^^^
    10 |   }
    11 |
    12 |   someDateFunction = (date: Date) => {
```

一般的にreactive化したオブジェクト型が元に戻せなくなるわけではなく、Date型特有の問題のように思われます。


# ググった
ググったら似たような問題がすぐに出てきました。

https://github.com/vuejs/core/issues/4307

引用
> the project had typescript version 4.1.6, from vue create. Аfter updating typescript to the latest version everything works. Thank you very much 

。。

確かにこちらのプロジェクトもnuxt-appで生成したもののようで、TypeScriptのバージョンは古いみたいですね。(こちらで使っていたのは`4.2`)

ですが、チーム開発しているのもあり、**TypeScriptのバージョンを変えたくない**。。。

# 天啓
悩んでいるとちょうど居合わせた開発部長が相談に乗ってくださり(**風通しの良い開発現場です。**)、いろいろ試してみた中で見つかったやり方が以下。

``` typescript
targetDate = reactive(new Date()) as Date
```

`~ as Date`でDate型であることを明記するというものです。
この書き方で、きちんとDate型として扱うことができました。めでたしめでたし。

ちなみに、↑の書き方ができるならこちらの書き方でもできるかと思いましたが、ダメでした。。
``` typescript
targetDate: Date = reactive(new Date())
```

# 何が違うのか
先ほどの２パターンは似たようなことをしているようにも思えますが、一体何が違ったのでしょうか。

``` typescript
targetDate: Date = reactive(new Date())
```

``` typescript
targetDate = reactive(new Date()) as Date
```

よく参考にしている[サバイバルTypeScript](https://typescriptbook.jp/reference/values-types-variables/type-assertion-as)によると、前者は**型アノテーション(型注釈)**、後者は**型アサーション**と呼ばれるものだそうです。

型アノテーションはコンパイラーの型推論のヒントとなるものです。

> 型注釈は、コンパイラーに「この変数に代入できるのはこの型だよ」と伝えるものです。コンパイラーは型注釈をヒントに、その型に値が代入可能かどうかをチェックし、代入できないことが分かり次第報告してきます。

一方、型アサーションはコンパイラーによる型推論を矯正する仕組みのようです。

> 型アサーションはコンパイラーに「君はこの型だと思ってるかもしれないけど、本当はこの型だよ」と型推論の不正確さを伝えるものです。

今回のケースでは、reactiveで宣言したDate型には正常に型推論が働かなかった(これはTypeScriptバージョン依存のバグ？)ため、型アサーションで無理やり矯正したというところでしょうか。

# よりよい解決策
型アサーションは強力ゆえに慎重な使用が求められ、やむを得ない場合を除いて使わない方がいいとのことです。型づけを無理やり変更してしまうため、想定通りの挙動にならない可能性があるからでしょう。

今回のケースだと、[型ガード](https://typescriptbook.jp/reference/statements/control-flow-analysis-and-type-guard)を使った条件分岐により、特定の型の場合のみ処理を適用するようにできます。(型アノテーションの章でも少し紹介されていました。)これにより、型づけによる安全性の担保をしながらコンパイルエラーを解消することができました。

``` States.ts
import {reactive} from "@nuxtjs/composition-api";

export class States {
  targetDate = reactive(new Date())

  someFunction = () => {
    if (this.targetDate instanceof Date) { // Date型のインスタンスに限定する条件分岐を追加
      this.someDateFunction(this.targetDate)
    }
  }

  someDateFunction = (date: Date) => {
    console.log(date)
  }
}
```

# おわりに
Kotlin + Spring Bootをメインに開発していたので、Nuxt.js + TypeScriptのフロントエンド開発は右も左もわからない状態で大変でしたが、新鮮で楽しいことも多いですね。特にJavaScriptのDateの挙動には驚かされています。
今回のプロジェクトで、エンジニアとしてできることがまた少し増えたと思うので、**希望すれば新しい分野に挑戦させてもらえる環境はありがたいなと強く感じました。**