【TypeScript】超実践的テクニック集【Reactなし】

## はじめに

この記事はニジボックスQiita記事投稿リレーの2日目の記事です🌈


TypeScript（Reactなし）のフロントエンド実務現場で1年半ほど業務してきたうえで、頻繁に使うTypeScriptのテクニック（tipsレベルですが）をまとめていきます。タイトルはちょい盛りです。
手続き型でDOMを直接ゴリゴリいじくるイベント駆動のスタイルで、いわゆるモダンフロントな現場ではないという点、あらかじめご承知おきください。

## 1. 自分で定義した型に型ガードする
### 1-1. ユーザー定義型ガードについて

TypeScriptでは`typeof`、`instanceof`、`in`などの演算子を用いて変数を型ガードできますが、これらの演算子では自分で定義した型へは型ガードすることができません。

```ts
type Hoge = "hoge" | "fuga";

const attr = document.querySelector(".hoge")?.getAttribute("name") ?? "";
if (typeof attr === Hoge) {
  // NG
  console.log(attr);
}
```

<details>
<summary>[型ガードとは]</summary>
<div>

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard

特定のブロックスコープ内で変数の型を制限する方法のこと。
条件式を用いて、その条件ブロック内での変数の型を制限します。

```ts
let hoge: number | string;

if (typeof hoge === "number") {
  // このブロック内では`hoge`は`number`型に制限される
  hoge.trim(); // OK
}
// ブロック外では`number | string`のUnion型
hoge.trim(); //NG: Property 'trim' does not exist on type 'string | number'.
```

</div>
</details>

このような場合は**ユーザー定義型ガード**を用いて型ガードします。
ユーザー定義型ガードとは、自分で定義した型に絞り込むための条件式を自分で作成し、その条件式をもって型ガードするという方法です。

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard

引数を受け取りその引数が自分で定義した型にマッチするかどうかの`boolean`を返す関数を作ります。その関数の戻り値の型を`is`演算子を用いて`{引数} is {型}`と指定します。
これにてユーザー定義型ガードの完成です。この関数に引数を渡して`true`を返す場合、引数`x`は自分で定義した型に型ガードされます。

```ts
type Hoge = "hoge" | "fuga";
const isHoge = (x: string): x is Hoge => ["hoge", "fuga"].some((val) => val === x);

let attr: string;
if (isHoge(attr)) {
  attr; // このブロック内では`"hoge" | "fuga"`に型ガードされる
}
```

とってもとってもよく使うテクニックです。
（[型定義からユーザー定義型ガードを自動生成してくれるライブラリ](https://github.com/d-kimuson/type-predicates-generator)なども存在します。）

### 1-2. Union 型のユーザー定義型ガードのテクニック

自分で定義したUnion型へ型ガードするときに使える有用なテクニックがあります。
**Union型の取りうる値を配列に格納し`typeof Array[number]`からUnion型を定義。その配列からユーザー定義型ガードを作成する**という方法です。

```ts
const hogeArray = ["hoge", "fuga"] as const; // constアサーションが必要
type Hoge = (typeof hogeArray)[number]; // Hoge = "hoge" | "fuga"
const isHoge = (x: string): x is Hoge => hogeArray.some((val) => val === x);

let attr: string;
if (isHoge(attr)) {
  attr; // このブロック内では`"hoge" | "fuga"`に型ガードされる
}
```

`const`アサーションした配列に対し`typeof Array[number]`とすると、その配列の要素のUnion型を取り出せます。この挙動については以下の記事が参考になります。

https://zenn.dev/luvmini511/articles/d89b3ad241e544

配列を一度定義することによって`Array.some()`から簡単にユーザー定義型ガードを作成できます。`array.some((val) => val === x)`が`true`の場合`x`は`array`の要素のいずれかと同じである、つまり`(typeof array)[number]`型である、ということになるためです。

この実装方法が有用な点は、型情報に変更があった際に配列の要素を修正するだけでよくユーザー定義型ガード側に修正を加える必要がないため、保守性が担保される点です。

## 2. DOMから取得した要素の型を`instanceof`演算子で絞り込む
こちらも型ガードに関連した内容です。
JavaScriptでは問題なくアクセスできていたDOMのプロパティに、TypeScriptではアクセスできない、ということが多々あります。

```ts
const anchorElm = document.querySelector(".anchorElm"); // なんらかのa要素の想定
anchorElm.href; // NG: Property 'href' does not exist on type 'Element'.
```

上記の例では`document.querySelector()`からDOM上の`a`要素を取得し`href`プロパティにアクセスしようとしていますが「`Element`型には`href`プロパティは存在しない」というエラーが発生しています。これは`document.querySelector()`の返り値が`Element`型であり、`href`プロパティは`Element`のインスタンスプロパティとして定義されていないことが原因です。

https://developer.mozilla.org/ja/docs/Web/API/Element#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

ここで`instanceof`演算子を使って要素を型ガードすることにより、`Element`からさらに継承先のクラスにのみ定義されているインスタンスプロパティにアクセスできます。
以下の例では、`Element`型を`href`プロパティが定義されている`HTMLAnchorElement`型に型ガードしています。

```ts
const anchorElm = document.querySelector(".anchorElm");
if (anchorElm instanceof HTMLAnchorElement) {
  anchorElm.href; // OK
}
```

https://developer.mozilla.org/ja/docs/Web/API/HTMLAnchorElement/href

`Property 'xxx' does not exist on type 'xxx'.`と怒られたら、アクセスしたいプロパティがどの型に定義されているかを確認し、`instanceof`演算子を使って要素を型ガードしましょう。

## 3. `Array.filter()`での型ガードの方法
### 3-1. `Array.filter()`の返り値の型を`is`演算子で指定

`Array.filter()`メソッドで配列のフィルタリングができますが、残念ながらそのままでは型情報のフィルタリングまではできません。

```ts
const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val) => typeof val === "string");
filtered; // (string | number)[] → string[]になってほしい
```

ここでもユーザー定義型ガード同様に`.filter()`メソッドの返り値の型を`is`演算子で指定することで、フィルタリング後の配列の要素の型ガードが可能です。

```ts
const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val): val is string => typeof val === "string");
filtered; // string[]
```

### 3-2. フィルタリングの条件に沿った型情報を返却するとよい

上記の例では「型が`string`の要素のみを抽出」という条件式なので、`is string`という型ガードが適切です。一方条件式を「型が`number`でない要素のみ抽出」という条件にした場合、型定義も「要素の型から`number`型を排除した型」というフィルタリングの条件に沿った型にするとよいです。


```ts
const array = [1, "2", "3", 4, 5];
const filtered = array.filter((val): val is Exclude<typeof val, number> => typeof val !== "number");
filtered; // string[]
```

「ある型から特定の型を排除したい」という場合は[Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)の`Exclude<T, U>`や`Omit<T, U>`を利用しましょう。


## 4. テンプレートリテラルを`const`アサーションするとより絞られた型情報になる

テンプレートリテラルで宣言した変数は`string`型となりますが、その変数を`const`アサーションすることでより絞られた文字列リテラル型となります。

```ts
const CONST_STR = "some_text";
const text = `this is ${CONST_STR}`;
text; // string

const CONST_STR = "some_text";
const text = `this is ${CONST_STR}` as const;
text; // "this is some_text"
```

「あれ？この変数ってどんな値が入ってくるんだっけ？」となった際に型情報からすぐに確認できるため可読性が向上します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1157551/810b9ff4-d4c8-912f-ac23-feb3233b95ce.png)
ちなみに、変数を挿入すると文字列リテラルのUnion型となります。

```ts
let str: "hoge" | "fuga";
const text = `this is ${str}` as const;
text; // "this is hoge" | "this is fuga"
```

## 5. `let`の型定義をスキップするテクニック
`let`で変数を宣言する場合はあらかじめ型情報を明示する必要があります。

```ts
let str: "hoge" | "fuga";
if (bool) {
  str = "hoge";
} else {
  str = "fuga";
}
```

上記のように特定の条件に応じて変数がすぐに決定し以降変更されない、という場合は`let`ではなく`const`で宣言し即時関数内で条件ごとの値を返却する、という手法をとることであらかじめ型定義する必要がなくなります。
また`const`ですのでイミュータブルであることが約束されており、可読性・保守性の向上も望めます。

```ts
const str = (() => {
  if (bool) {
    return "hoge";
  } else {
    return "fuga";
  }
})();
str; // "hoge" | "fuga"
```

自分で型定義をしていない分、条件が増えた時に柔軟に推論される点もよいです。

```ts
const str = (() => {
  if (bool1) {
    return "hoge";
  } else if (bool2) {
    return "fuga";
  } else {
    return "piyo";
  }
})();
str; // "hoge" | "fuga" | "piyo"
```

## 6. 標準APIのコンストラクターに渡す引数は変数として括り出さずに直接記述する

あくまで場合によりけりだとは思いますが、標準APIのコンストラクターに渡す引数は変数として括り出さずに直接記述したほうがなにかとよかったりします。
以下ではWebAPIの[`IntersectionObserver`](https://developer.mozilla.org/ja/docs/Web/API/IntersectionObserver/IntersectionObserver)コンストラクターにコールバック関数を渡している例です。

```ts: コールバック関数を変数で括り出す場合
const callback = (entries: IntersectionObserverEntry[]) => {
  // ...
};
const observer = new IntersectionObserver(callback);
```

標準APIの引数に渡す変数・関数は型を一致させる必要があるため、その都度適切に明示しなければなりません。覚えていなければ、都度調べる必要があります。
一方変数として括り出さずに引数へ渡す際に記述すれば、TypeScriptが自動で推論してくれるため型を明示する必要がありません。

```ts: コールバック関数を直接記述する場合
const observer = new IntersectionObserver((entries) => {
  // entriesがIntersectionObserverEntry[]に自動推論される
  // ...
});
```

エディターによってはサジェストしてくれるため、タイポも防げてとてもよいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1157551/11d5a385-a4ae-94fd-b9c5-169919f534e6.png)

## おわりに
私自身が実務で頻繁に使うTypeScriptのテクニック（tips）をまとめてきました。
本記事でまとめた内容がどなたかの助けになれば幸いです。


:::note info
この記事はニジボックス投稿リレー企画、2日目の記事です。
次の投稿は @t_kokubo さんの『[homebrewでnodenvを更新する際の注意点(shimsの再生成)](https://qiita.com/t_kokubo/items/d5d2a6e67be08b7b88a9)』です！
:::