妥協しないTypescript

[テックタッチアドベントカレンダー](https://qiita.com/advent-calendar/2019/techtouch)5日目を担当する@takakobemです。
4日目は @terunuma による [4Kモニタ環境で1年間Web開発してみた所感](https://qiita.com/terunuma/items/3eede74b6354da113d66) でした。
4Kモニタいいですよね。ただ、以前私は43型の4Kモニタを使っていたのですが、正直でかすぎて画面の端が見づらかったです。。30インチぐらいがベストかもしれませんね。

# まえがき
本記事はTypescriptを触ったことがある人を対象にしています。
「Typescriptは使っているけど、ちゃんと使いこなせているかわからない」という人に一番しっくりくる内容だと思います。
私が以前C++を触っていたこともあり、結構C++を引き合いに出しています。
また、私がReactやReduxを触っていることもあり、オブジェクト指向よりも関数型プログラミングを意識したものになっています。

# はじめに

Typescriptは非常に便利なツールです。
Javascriptそれ自体は静的な型チェックができないため、コーディング中に実装ミスに気づきにくいのですが、Typescriptを導入することで型チェックが効くようになり、VSCodeなどのエディターを使えば補完も利用できるようになります。
ただ、TypescriptはC++やJavaなどといった言語とは違い、**型の定義が中途半端でも動いてしまいます**。
一見型をしっかりつけているつもりで書いていても、ちょっとしたことで信頼性がないコードができあがってしまいます。
今回は**型を妥協してしまっている書き方**を**Badケース**として例に上げ、どうすればよいかを**Goodケース**として紹介していきます。

※本記事はTypescript3.7時点のものです。

# 1. anyは使わない

Typescriptを触ったことがある人であれば、`any`は知っているかと思います。
極端に言ってしまうと、**`any`は型を付けるのを放棄したのと同義**です。
Typescript は Javascript に型を付けるための言語なのに、型を付けるのを放棄してしまっては元も子もありません。
Typescript で型を付けることを選んだのであれば、`any`を使うことは避けましょう。

### Bad :cry: 

`number`を引数に取りたいのに、`any`にしてしまうと`string`型でも通ってしまいます。

```typescript
declare function hoge(num: any): void
hoge(1) // ok
hoge("hoge") // これもokになってしまう
```

### Good :smile:
ちゃんと型を定義してあげましょう。

```typescript
declare function fuga(num: number): void
fuga(1) // ok
fuga("fuga") // error
```

# 2. `!`は使わない
`!`は[non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator)と呼ばれています。
これをつけることで、`null`かもしれないようなものを強制的に`null`じゃないものとしてみなすことができます。
「ここは`null`はありえないはずだから〜」という理由でよく使いがちなのですが、ありえないと思ってしまっているだけかもしれませんし、今後`null`が入ってしまっても気づけなくなりますよね。
なので、そもそも`null`をとらないようにしてしまうか、ちゃんと実行時にチェックをするべきです。
Typescript3.7からの機能である[Optional Chaining](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining)を使うのもありです。

### Bad :cry:

```typescript
function hoge(str: string | null) {
  str.toUpperCase() // Object is possibly 'null'.
  str!.toUpperCase() // ok
}
```

### Good :smile:

ちゃんと実行時にチェックしてあげましょう。
もしくはnullをとらないようにしてあげましょう。

```typescript
function hoge(str: string | null) {
  if (str !== null) {
    str.toUpperCase() // ok
  }
}

// or

function fuga(str: string | null) {
  str?.toUpperCase() // ok
}

// or

function fuga(str: string) {
  str.toUpperCase() // ok
}
```

# 3. `{}`を使う時は注意

例えばkeyに名前を持ち、valueに年齢を持つようなオブジェクトを定義したいとします。
keyは名前だから`string`, valueは年齢だから`number`と安直にやってしまうと、型安全が崩れてしまいます。

### Bad :cry:

安直に上記の通りに定義してみます。

```typescript
const ageData: Record<string, number> = {
  yamada: 10,
  tanaka: 20
}
console.log("山田さんの年齢=", ageData.yamada)  // 10
console.log("田中さんの年齢=", ageData.tanaka)  // 20
console.log("鈴木さんの年齢=", ageData.suzuki)  // undefined
```
この時、山田さんのデータは入っていますが、鈴木さんのデータは入っていないため、鈴木さんの年齢は`undefined`として返ってきます。
しかし、Typescript的にはエラーと認識してくれません。

### Good :smile:

keyが予め予測可能なものであれば、keyを定義しておきましょう。

```typescript
const ageData: Record<"yamada" | "tanaka", number> = {
  yamada: 10,
  tanaka: 20
}
console.log("鈴木さんの年齢=", ageData.suzuki)  // Property 'suzuki' does not exist on type 'Record<"yamada" | "tanaka", number>'.(2339)
```

idなど、keyが事前に定義不能な場合は、valueに`undefined`も定義しておきましょう。

```typescript
const ageData: Record<string, number | undefined> = {
  yamada_id: 10,
  tanaka_id: 20
}

console.log("鈴木さんの年齢=", ageData.suzuki + 1) // Object is possibly 'undefined'. 
```

# 4. `as`は使わない

以下のような定義があったとしましょう。

```typescript
type Base = {
  x: number
}

type Derived = Base & {
  y: number
}
```

これは、オブジェクト指向でいう基底クラスと派生クラスの関係に近いです。
Typescript では、このように [Intersection Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types) を使うことで継承のようなものを表現することができます。
ここで問題なのが、**`as`を使うことでこれら２つの型が双方向にキャストできてしまう**ということです。

### Bad :cry:

例えば、以下のようなアップキャストは問題なく行なえます。

```typescript
const derived: Derived = {
    x: 1,
    y: 2
}
const base = derived as Base
console.log(base.x) // 1
```

元の`derived`には`x`が含まれているので何も問題ありませんね。
次はダウンキャストです。

```typescript
const base: Base = {
    x: 1,
}
const derived = base as Derived // ダウンキャストができてしまう
console.log(derived.y) // undefined
```

`base`には`y`が無いのに、エラーなく実行されてしまいます。
**`as`を使うとダウンキャストがすんなりできてしまいます**。

### Good :smile:

ではどうすればよいのか。
いえ、どうもしなくていいんです。
そもそもTypescriptが提供するものはただの型です。
継承といっても、カプセル化や関数のオーバーライドなどはありません。
そもそもキャストする必要がないのです。

ただ、引数に基底クラスを受け、条件に応じて派生クラスに変換し結果を返したいようなケースはありますよね。それを次に説明します。

# 5. Union Types + String Literalを使いこなす

以下のような、CircleとSquareという型があるとします。

```typescript
type Circle = {
  radius: number
}

type Square = {
  height: number
  width: number
}
```

これはどちらも図形ですね。Typescriptでは以下のように[Union Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types)を使うことで、異なる2つの型を1つの型として扱うことができます。

```typescript
type Figure = Circle | Square
```

これは、**派生クラスと派生クラスから基底クラスを作るようなもの**です。
C++から入った僕は非常に理解に苦しみました。
Typescript恐ろしい。。

### Bad :cry:

このようにすることで、例えば以下のように図形を引数にその面積を返すような関数が定義できます。

```typescript
function area(figure: Figure): number {
  if ("height" in figure) {
    return figure.width * figure.height
  }
  if ("radius" in figure) {
    return figure.radius * figure.radius * Math.PI
  }
  throw new Error("no such figure")
}
```

あれ？なんか奇妙ですよね。
そう、上記の例では、**図形に特定のプロパティがあるかどうか**で図形を判定し、面積を計算しています。
でもこれって気持ち悪いですよね。例えば`Triangle`という型が追加になると、`Triangle`も`height`を持つので、条件式を見直さなければならなくなります。
「クラスを使ってメンバ関数で計算しろよ」って言われそうですが、クラスにしなくてもオブジェクトにちょこっと細工することで解決できちゃうんです。
（なぜクラスを頑なに使わないのか、React/Redux使いの人なら知ってるかと思いますが、その理由はまたいつか機会があれば記事書きます。）

### Good :smile:

ではどうするかというと、こういう時は **その型が何か** を [string literal](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types) で定義しておけばよいです。

```typescript
type Circle = {
  kind: "circle"
  radius: number
}

type Square = {
  kind: "square"
  height: number
  width: number
}
```

こうすることで、プロパティの存在によってif文を分けなくても、`kind`というkeyによってswitch文で処理を分けることができるようになります。
また、例えば `case "circle"` 内で `figure.width` にアクセスしようとするとエラーになります。

```typescript
function area(figure: Figure): number {
  switch (figure.kind) {
    case "circle":
      // figure.width // Property 'width' does not exist on type 'Circle'.
      return figure.radius * figure.radius * Math.PI
    case "square":
      return figure.height * figure.width
  }
}
```

Typescript賢すぎる！

# 最後に

いかがでしたでしょうか。
Typescriptは非常に便利なツールですが、使いこなすにはいくつかコツがいりますし、正直難しいです。
今回挙げた内容もTypescriptができることのほんの一部に過ぎません。
しかし、使いこなせれば非常に便利なツールです。こんなことできるかな？と思ったことが大抵できますので、是非いろいろ試してみてください。

6日目は @ihiroky による「JSXとvirtual-domで遊ぶ」です。