ちょっとだけ幸せになれるTypeScript

## 想定対象読者
- JavaScriptある程度書ける人
- TypeScript書いたことが無い、または始めたばかりの人

## 環境
- TypeScript 3.6.3
    - strict: true

本校に掲載されているソースコードは [TypeScript Playground](https://www.typescriptlang.org/play/) に貼り付けることで、実際の動作を試すことができます。

## TypeScriptの幸せの定義
TypeScriptにおける幸せとは、

**JavaScriptだったらランタイムエラーになるコードが
TypeScriptにしたおかげで、コンパイルエラーとして開発中に検知できること**

です。

例を見てみます。

下記JavaScriptのプログラムは、文字列ではない`thisIsNumber`に対して`toUpperCase()`を呼んでしまっており、実行時エラーになります。

```js:JavaScript
const thisIsString = "1";
const thisIsNumber = 1;

thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // ランタイムエラー
```

一方**Type**Scriptの下記プログラムでは、number型に対して`toUpperCase()`が存在しないことが静的型解析によって事前に分かるため、コンパイル時にエラーとなります。

```ts:TypeScript
const thisIsString: string = "1";
const thisIsNumber: number = 1;

thisIsString.toUpperCase(); // OK
thisIsNumber.toUpperCase(); // コンパイルエラー(Property 'toUpperCase' does not exist on type 'number'.)
```
このように**コンパイルエラーとして開発中に検知できるようにすること**が、TypeScriptとの上手な付き合い方です。

### 不幸せになる方法
**any型を使う**ことで、いとも簡単に幸せを手放すことができます。

[Basic Types#any · TypeScript](https://www.typescriptlang.org/docs/handbook/basic-types.html#any)

```ts:TypeScript
const thisIsNumber: any = 1;

thisIsNumber.toUpperCase(); // コンパイルエラーは出ず、ランタイムエラー
```
`thisIsNumber`をnumber型としていたらコンパイルエラーとして検知できていたのに、any型として定義してしまった場合はコンパイラの型チェックをパスしてしまいます。


しかし、any型を使うのはやむを得ないときもあります。
any型の**コンパイルが通るようになる**という強力な効果はJavaScriptの記法との互換性を獲得できるため、使うことで一時的にでも生産性が上がるのは否めません。


以下本稿では、やむを得ない時を少しでも減らしてTypeScriptで幸せになれる方法を、紹介していきます。

## 幸せになるためのTips

### Union Types
型定義を記述しようにも「文字列か数値かどっちになるか分からない引数・変数」が出てくる時があると思います。

やむを得ず`any`を使いたくなります。

```ts
const thisIsStringOrNumber: any = "1";
```

`any`を使っていても、下記のように`typeof`を使うことで型を絞り込むことができ、型安全に使えるケースもありますが...

```ts
// ※`declare`は今回の本質ではないので気にしないでください
declare const thisIsStringOrNumber: any;

if (typeof thisIsStringOrNumber === "string") {
  // string型と推論されるので、String.prototype.toUpperCase()を使ってもコンパイルOK!
  thisIsStringOrNumber.toUpperCase();
} else if (typeof thisIsStringOrNumber === "number") {
  // number型と推論されるので、Number.prototype.toFixed()を使ってもコンパイルOK!
  thisIsStringOrNumber.toFixed();
} else if (typeof thisIsStringOrNumber === "boolean") {
  // ここを通ることはあるのか？
} else {
  // ここを通ることはあるのか？
}
```

ここでは**Union Typesを使います。**
[Advanced Types#Union Types · TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types)

Union Typesを使うと、複数の型の和を表現できます。
この場合は「`string`か`number`のどちらか」という型を`string | number`で表現できます。

```ts
declare const thisIsStringOrNumber: string | number;

if (typeof thisIsStringOrNumber === "string") {
  // string型と推論されるので、コンパイルOK!
  thisIsStringOrNumber.toUpperCase();
} else {
  // これだけでnumber型と推論されるので、コンパイルOK!
  thisIsStringOrNumber.toFixed();
}
```
`else`の部分で、`typeof thisIsStringOrNumber === "number"`といった条件文を書いていないのにnumber型として変数を扱うことができている点が重要です。

### Unknown Type
たとえばJSONを文字列として受け取るとき

```ts
const val1 = JSON.parse('1');
const val2 = JSON.parse('2');

console.log(val1 + val2); //=> stringの'12' か numberの3 かは分からないが、エラーは出ない

val1.toFixed();
val2.toUpperCase(); // ここでランタイムエラー
```
意図していない型で演算しているときはコンパイルエラーになって欲しいですが、エラーが出ずランタイムエラーになってしまいます。


これは、[TypeScriptにおける`JSON.parse`の型定義が`any`を返すようになっている](https://github.com/microsoft/TypeScript/blob/v3.6.3/lib/lib.es5.d.ts#L1059)事が原因です。

```ts
JSON.parse(text: string): any
```
何が返ってくるかわからない値に対して、`any`を使わずコンパイルエラーが出るようにしたいです。

こういったケースの対策として、**unknown型**が導入されました。
[TypeScript 3\.0#New unknown top type · TypeScript](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type)

`unknown`はどんな値にも適用できるという点では`any`と同様ですが、型が判明するまではいかなる演算にも使うことができずコンパイルエラーになる、という点が異なります。

```ts
const val1: unknown = JSON.parse('1');
const val2: unknown = JSON.parse('2');

console.log(val1 + val2); //=> コンパイルエラー

if (typeof val1 === "number" && typeof val2 === "number") {
  // OK!
  console.log(val1 + val2);
} else if (typeof val1 === "string" && typeof val2 === "string") {
  // OK!
  console.log(`${val1}${val2}`);
}
```

### Literal Types
たとえば下記のようなOffice製品を文字列として引数に取る関数に、`Keynote`という知らない製品が渡されたときのことを考えます。

```ts
const func =  function(p: string) {
  if (p === 'Word') return 'わーぷろ！';
  if (p === 'Excel') return '表計算！';
  if (p === 'PowerPoint') return 'プレゼン！';

  throw new Error('予期しない製品です');
}

func('Word'); //=> わーぷろ！
func('Excel'); //=> 表計算！
func('Keynote'); //=> ランタイムエラー(予期しない製品です)
```
これは、引数が`'Word'`,`'Excel'`,`'PowerPoint'`という3種類しか期待していないのに、`string`型となっており無数の文字列を受け付けるようになっているのが、ランタイムエラーの原因です。

ここでは`'Word'`,`'Excel'`,`'PowerPoint'`という**String Literal**を定義し、それらのUnion Typesを引数の型として指定することで、予期しない引数をコンパイルエラーで検知できます。
[Advanced Types#String Literal Types · TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types)

```ts
type Office = 'Word' | 'Excel' | 'PowerPoint';

const func =  function(p: Office) {
  if (p === 'Word') return 'わーぷろ！';
  if (p === 'Excel') return '表計算！';
  if (p === 'PowerPoint') return 'プレゼン！';

  throw new Error('予期しない製品です');
}

func('Word'); //=> わーぷろ！
func('Excel'); //=> 表計算！
func('Keynote'); //=> コンパイルエラー
```

### Index Type Query
本稿の話の応用編です。
下記のようなオブジェクトを考えてみます。

```ts
type Props = {
  name: string;
  age: string;
  gender: string;
}
const obj: Props = { name: "suzuki", age: "28", gender: "male" };

const describe = function (key: string) {
  // any使わざるを得ない...？
  return `${key} is ${(obj as any)[key]}`;
};

describe('name'); // name is suzuki
describe('age'); // age is 28
describe('job'); // jobは無いので、job is undefined となってしまう
```
`obj[key]`が何か分からないのでanyでキャストしないとコンパイルエラーになる点と、`obj["job"]`は存在せずundefinedになってしまう点が不幸せなところです。
関数の引数が`string`型になっているため、これをString Literail Typesとして絞り込みたいです。そのまま`"name" | "age" | "gender"`と書くことでもランタイムエラーを回避できるのですが、Propsの定義とダブルメンテになってしまいます。

ここで**keyof**を使うと、定義済の型のキーを`String Literal`の`Union Types`として得ることができます。
[TypeScript 2\.1#Index Type Query · TypeScript](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types)

```ts
type Props = {
  name: string;
  age: string;
  gender: string;
}

const obj: Props = { name: "suzuki", age: "28", gender: "male" };

// key: "name" | "age" | "gender" と推論される
const describe = function (key: keyof Props) {
  return `${key} is ${obj[key]}`;
};

describe('name'); // name is suzuki
describe('age'); // age is 2
describe('job'); // コンパイルエラー(Argument of type '"job"' is not assignable to parameter of type '"name" | "age" | "gender"')
```

ReactでPropsの型定義をした時などに有効です。[Material-UIのドキュメント](https://material-ui.com/ja/components/text-fields/)のTypeScriptサンプルコードでも使われている方法です。

引用: https://github.com/mui-org/material-ui/blob/v4.5.1/docs/src/pages/components/text-fields/TextFields.tsx

```typescript:ocs/src/pages/components/text-fields/TextFields.tsx
interface State {
  name: string;
  age: string;
  multiline: string;
  currency: string;
}

export default function TextFields() {

  // 中略

  const handleChange = (name: keyof State) => (event: React.ChangeEvent<HTMLInputElement>) => {
    setValues({ ...values, [name]: event.target.value });
  };
}

```

TypeScriptで幸せになろう！

## 参考文献
- [ConditionalTypes I/O - TypeScript3.4 型の強化書](https://booth.pm/ja/items/1317204)
- [実践TypeScript](https://www.amazon.co.jp/dp/483996937X/)