229
239

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Last updated at Posted at 2023-06-26

はじめに

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

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

1. 自分で定義した型に型ガードする

1-1. ユーザー定義型ガードについて

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

type Hoge = "hoge" | "fuga";

const attr = document.querySelector(".hoge")?.getAttribute("name") ?? "";
if (typeof attr === Hoge) {
  // NG
  console.log(attr);
}
[型ガードとは]

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

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

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'.

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

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

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"`に型ガードされる
}

とってもとってもよく使うテクニックです。
型定義からユーザー定義型ガードを自動生成してくれるライブラリなども存在します。)

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

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

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型を取り出せます。この挙動については以下の記事が参考になります。

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

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

2. DOMから取得した要素の型をinstanceof演算子で絞り込む

こちらも型ガードに関連した内容です。
JavaScriptでは問題なくアクセスできていたDOMのプロパティに、TypeScriptではアクセスできない、ということが多々あります。

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のインスタンスプロパティとして定義されていないことが原因です。

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

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

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

3. Array.filter()での型ガードの方法

3-1. Array.filter()の返り値の型をis演算子で指定

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

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

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

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型を排除した型」というフィルタリングの条件に沿った型にするとよいです。

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

「ある型から特定の型を排除したい」という場合はUtility TypesExclude<T, U>Omit<T, U>を利用しましょう。

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

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

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
ちなみに、変数を挿入すると文字列リテラルのUnion型となります。

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

5. letの型定義をスキップするテクニック

letで変数を宣言する場合はあらかじめ型情報を明示する必要があります。

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

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

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

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

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

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

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

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

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

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

エディターによってはサジェストしてくれるため、タイポも防げてとてもよいです。
image.png

おわりに

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

この記事はニジボックス投稿リレー企画、2日目の記事です。
次の投稿は @t_kokubo さんの『homebrewでnodenvを更新する際の注意点(shimsの再生成)』です!

229
239
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
229
239

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?