TypeScript の気をつけたい機能

# はじめに

TypeScript には間違った使い方をすると型の恩恵が受けられなくなるものの、正しく使えば便利な機能がいくつかあります。

また特に Vue においては比較的 `as` などを使う機会が多く心理的ハードルが下がり間違ったケースでも使われてしまうケースが多いように感じます。

そこでどういう使い方が間違っていて、どう使うのが正しいのかをまとめ、正しく TypeScript の機能を使いましょうということでこの記事を書きました。


# TypeScript の気をつけたい機能

## `any`

いわずもがな。
あらゆるデータを受け付ける型で、乱用すると TypeScript を使っている意味がなくなってしまうため `any` であることが正しいと説明できる場合以外は **できる限り利用は避けましょう**。


## `as` : 型アサーション、 Type Assertion 

`as` は正しく型推論できない場合などに **型をコンパイラに教える** ための機能です。
ただし **値が変換されるわけではない** ため間違った型を設定すると、実際のデータと型が一致しなくなり **実行時エラーとなる場合がある** ので気をつけましょう。

### <font color="#ff1744">✗</font> Bad

`user` は `User` 型であると `as` を使って コンパイラに教えているため `user.profile.gender` はコンパイルエラーになりませんが、実際は `profile` プロパティを持っていないため実行時エラーとなります。

```ts
type User = {
  id: number;
  name: string;
  profile: {
    gender: string;
  }
}

const user = {
  id: 1,
  name: "yamagen0915",
} as User;

console.log(user.profile.gender);
// Uncaught TypeError: Cannot read property 'gender' of undefined
```

### <font color="#4caf50">○</font> Good

どう対処すべきかはケースバイケースですが、


```ts
// 型定義に合わせ profile を設定する
const user: User = {
  id: 1,
  name: "yamagen0915",
  profile: { 
    gender: 'male'
  }
}
```

## `!.` : Non Null Assertion

**Nullable でないことをコンパイラに教える** 機能です。
(Nullable = `null` or `undefined`)
`as` と同様 Nullable になり得る場所で使ってしまうと **実行時エラーとなる場合がある** ので気をつけましょう。

### <font color="#ff1744">✗</font> Bad

`profile?` へ `!.` でアクセスしているためコンパイルエラーになりませんが、実際は `profile` プロパティを持っていないため実行時エラーとなります。

```ts
type User = {
  id: number;
  name: string;
  profile?: {
    gender: string;
  }
}

const user: User = {
  id: 1,
  name: "yamagen0915",
};

console.log(user.profile!.gender);
// Uncaught TypeError: Cannot read property 'gender' of undefined
```


### <font color="#4caf50">○</font> Good

null チェックを行うようにしましょう。

```ts
if (user.profile) {
  console.log(user.profile.gender);
}
```

# Vue の具体例

Vue2, Vue3 Composition API において具体的にどのようにそれらの機能を使うか、または使わないべきかを紹介していきます。

## methods や関数の引数

**関数の引数は型を明示する必要** があります。

### <font color="#ff1744">✗</font> Bad

引数に型を明示しないと `any` となります。

```ts
export default Vue.extend({
  methods: {
    // id: any, name: any
    foo(id, name) {}
  }
});

// id: any, name: any
const foo = (id, name) => {}
```

### <font color="#4caf50">○</font> Good


```ts
export default Vue.extend({
  methods: {
    foo(id: number, name: string) {}
  }
});

const foo = (id: number, name: string) {}
```

### <font color="#4caf50">○</font> Good

関数の引数に渡す関数などはその引数の定義から型推論されます。

```ts
// n: number
[1, 2, 3, 4, 5].map(n => n + 1);
```
### <font color="#4caf50">○</font> Good

デフォルト値が存在する場合はデフォルト値の型から型推論されます。

```ts
export default Vue.extend({
  methods: {
    // id: number, name: string
    foo(id = 0, name = '') {}
  }
});

// id: number, name: string
const foo = (id = 0, name = '') => {}
```

## template ref

template ref では **as を利用しコンパイラに型を教える必要** があります。

### <font color="#ff1744">✗</font> Bad

```ts
// <input ref="input">
// input: Vue | Vue[] | Element | Element[]
const input = this.$refs.input

// <custom-component ref="custom"/>
// custom: Vue | Vue[] | Element | Element[]
const custom = this.$refs.custom
```

### <font color="#4caf50">○</font> Good

取得する値に応じて適切な型を `as` を使って設定しましょう。
ただし `$refs` で取得できるデータに変更があってもコンパイルエラーにはならないため気をつけましょう。

ちなみに input <-> HTMLInputElement のような対応は下記の定義通りです。

https://github.com/microsoft/TypeScript/blob/c9422e6aa14965b2ca938ad26f84a9164a324cfd/lib/lib.dom.d.ts#L19263-L19383

```ts
// <input ref="input">
// input: HTMLInputElement
const input = this.$refs.input as HTMLInputElement

// <custom-component ref="custom"/>
// custom: CustomComponent
const custom = this.$refs.custom as InstanceType<typeof CustomComponent>
```

## Composition API template ref 

Coposition API では ref を使って template ref と連携しますが、その ref の型引数に型を指定することができます。
ただしその変数の **宣言時には値を入れることができないため Nullable になります。**
都度 null チェックを行うのは面倒なため **Non Null Assertion の利用を検討しましょう**。


### <font color="#4caf50">○</font> Good

Non Null Assertion を利用せず都度 null チェックを行ってももちろん問題ありません。

```ts
setup() {
  // input: HTMLInputElement | undefined
  const input = ref<HTMLInputElement>(); 

  // custom: CustomComponent | undefined
  const custom = ref<InstanceType<typeof CustomComponent>>(); 

  // 例えばボタンクリックで input, custom へアクセスする場合
  const click = () => {
    if (input.value) {
      console.log(input.value.value);
    }
    if (custom.value) {
      console.log(custom.value.foo);
    }
  };

  return {
    input,
    custom,
    click,
  };
}
```

### <font color="#4caf50">○</font> Good

template ref の設定が適切な前提で Non Null Assertion を利用することも検討しましょう。
個人的には template ref の設定が間違っている場合に null チェックを行うと実行時エラーになりませんが、エラーになったほうがよいと思うのでこちらの書き方のほうがよいと思っています。

```ts
setup() {
  // input: HTMLInputElement | undefined
  const input = ref<HTMLInputElement>(); 

  // custom: CustomComponent | undefined
  const custom = ref<InstanceType<typeof CustomComponent>>(); 

  // 例えばボタンクリックで input, custom へアクセスする場合
  const click = () => {
    console.log(input.value!.value);
    console.log(custom.value!.foo);
  };

  return {
    input,
    custom,
    click,
  };
}
```

## `data()`

期待通りに型推論されないこともあるため **変数として切り出したり `as` を使って型を明示する必要** があります。
最終的にオブジェクトの型が正しく型推論されれば良いので対処方法は様々です。


### <font color="#ff1744">✗</font> Bad

`[]`, `undefined`, `null` は期待通りに型推論されません。

```ts
data() {
  return {
    list: [], // any[]
    id: undefined, // undefined
    name: null, // null
  };
}
```

### <font color="#4caf50">○</font> Good

各プロパティを変数として切り出し型を明示したり、`undefined`, `null` の利用をさけるようにしましょう。

```ts
data() {
  const list: string[] = [];
  return {
    lisｔ, // string[]
    id: 0, // number
    name: '', // string
  };
}
```

### <font color="#4caf50">○</font> Good

`as` を使って型を明示しましょう。
また初期値として `undefined`, `null` を使う必要がある場合は `| null` を忘れずにつけるようにしましょう。

```ts
data() {
  return {
    list: [] as string[],  // string[]
    id: undefined as number | undefined, // number | undefined
    name: null as string | null, // string | null
  };
}
```

### <font color="#ff1744">✗</font> Bad

オブジェクトの型を `as` で明示する場合、型の変更があった場合にエラーとならないため避けたほうが良い場合があります。

```ts
type User = {
  id: number;
  name: string;
};

data() {
  return {
    // 仮に gender: string が追加されてもエラーとならず型と値が一致しなくなってしまう。
    user: {
      id: 1,
      name: "yamagen0915",
    } as User,
  };
}
```

### <font color="#4caf50">○</font> Good

変数として切り出し型を明示することで型に変更があった場合にコンパイルエラーとすることができます。

```ts
type User = {
  id: number;
  name: string;
};

data() {
  // 仮に gender: string が追加された場合にエラーとなる
  const user: User = {
    id: 1,
    name: "yamagen0915",
  };

  return {
    user,
  };
}
```

## axios

Vue, TypeScript と直接は関係ありませんが API へのリクエストを axios を利用しているプロジェクトも多いと思うので紹介します。
`axios.get` 等には **レスポンスの型を型引数として指定することができる** ため設定しましょう。


### <font color="#ff1744">✗</font> Bad

型引数に何も指定しない場合 `response.data` は `any` となります。

```ts
const response = await axios.get('/api/users');
response.data // any
```

### <font color="#4caf50">○</font> Good

型引数に API のレスポンスの型を指定します。
ただし API のレスポンスが本当にその型の通りかどうかは保証されないため API の仕様が変わった場合は合わせて型定義も合わせて修正するようにしましょう。


```ts
type User = {
  id: number;
  name: string;
};

const response = await axios.get<User[]>('/api/users');
response.data // User[]
```