はじめに
この記事はニジボックスQiita記事投稿リレーの2日目の記事です🌈
TypeScript(Reactなし)のフロントエンド実務現場で1年半ほど業務してきたうえで、頻繁に使うTypeScriptのテクニック(tipsレベルですが)をまとめていきます。タイトルはちょい盛りです。
手続き型でDOMを直接ゴリゴリいじくるイベント駆動のスタイルで、いわゆるモダンフロントな現場ではないという点、あらかじめご承知おきください。
1. 自分で定義した型に型ガードする
1-1. ユーザー定義型ガードについて
TypeScriptではtypeof
、instanceof
、in
などの演算子を用いて変数を型ガードできますが、これらの演算子では自分で定義した型へは型ガードすることができません。
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
の場合x
はarray
の要素のいずれかと同じである、つまり(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 TypesのExclude<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"
「あれ?この変数ってどんな値が入ってくるんだっけ?」となった際に型情報からすぐに確認できるため可読性が向上します。
ちなみに、変数を挿入すると文字列リテラルの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[]に自動推論される
// ...
});
エディターによってはサジェストしてくれるため、タイポも防げてとてもよいです。
おわりに
私自身が実務で頻繁に使うTypeScriptのテクニック(tips)をまとめてきました。
本記事でまとめた内容がどなたかの助けになれば幸いです。
この記事はニジボックス投稿リレー企画、2日目の記事です。
次の投稿は @t_kokubo さんの『homebrewでnodenvを更新する際の注意点(shimsの再生成)』です!