型アサーションの正しい使い方
TypeScript
2025-05-18
はじめに
これまで、なんとなくの雰囲気でTypeScriptを書いており、TypeScriptらしいコードや型システムの活用について深く意識することなく開発をしていました。
もっとTypeScriptで良いコードが書けるようになりたいと思い、『Effective TypeScript 第2版』を読みはじめ、その中でこれまで曖昧に理解していたことが明確になりました。
特に型アサーションとの向き合い方は大きな学びがあったため、本記事では、その気づきや知見を整理してまとめたいと思います。
型の活用について
まず、TypeScriptで型を扱うときには、次の優先順位を意識したいです。
- 型推論:型システムを活用して、自動的に型を導き出す。まずはこれを最大限活用
- 型アノテーション:明示的に型を書く必要がある場合に補足する
- 型アサーション:値の型をTyepScirpt以上に、実装者が本当に把握しているときのみに使用する
型アサーションは、型システムが認識している型を上書きしてしまうので、利用法を誤ると型システムによる型安全性が失われるため最後の手段として扱うべきです。
型アサーションの良くない使用例
変数宣言時に型アサーションを使用している
- 【変数宣言時の型チェックが無効になるパターン】
変数宣言時に、型アサーションを使用すると本来は型エラーが発生するはずが、型システムが認識している型を上書きしてしまうためエラーにならない
interface Person { name: string };
// nameプロパティが必要なためエラーが発生
const alice: Person = {};
// ~~~~~ Property 'name' is missing in type '{}' but required in type 'Person'
// {}を型アサーションでPerson型に上書きしているためエラーが発生しない
const bob = {} as Person;
- 【余剰プロパティチェックが無効になるパターン】
また、型アノテーションを使うと、余剰プロパティチェックという存在しないプロパティ使用時に型エラーとするチェッカーが効かなくなります。
// Person型にはoccupationプロパティは定義されていないためエラーが発生
const alice: Person = {
name: 'Alice',
occupation: 'TypeScript developer'
// ~~~~~~~~~ Object literal may only specify known properties,
// and 'occupation' does not exist in type 'Person'
};
// 型アサーションを使った場合はエラーにならない
const bob = {
name: 'Bob',
occupation: 'JavaScript developer'
} as Person;
関数の戻り値の型を型アサーションで補足している
外部APIを叩く関数や際や、YAMLパーサー関数など、戻り値の型が不明確な場合に、関数を利用している側で、型アサーションを使って型を補足しているコードをたまに見かけるが、これは避けた方が良いです。
interface User {
name: string
age: number
}
async function fetchUserJSON(userId: string): Promise<unknown> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Unable to fetch! ${response.statusText}`);
}
return response.json();
}
// fetchUserJSONを利用する側で、型アサーションが必要になる
export async function fetchUser(userId: string): Promise<User> {
return fetchUserJSON(`/api/users/${userId}`) as Promise<User>
}
このような場合は関数本体に型アサーションを追加し、型アサーションが関数の実装に隠蔽されるようにすることで、呼び出し元での型アサーションが不要になり、型アサーションの利用が局所的になります。
interface User {
name: string
age: number
}
async function fetchUserJSON(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Unable to fetch! ${response.statusText}`);
}
return response.json() as Promise<User>
}
export async function fetchUser(userId: string): Promise<User> {
return fetchUserJSON(`/api/users/${userId}`)
}
型アサーションを使うべき場面
値の型をTypeScript以上に、実装者が本当に把握しているとき
- 【操作するDOM要素の型を扱う場合】
TypeScriptはDOM要素にアクセスできないため、実装者の方が型を把握しているケースが多いので問題ない。
document.querySelector('#myButton')?.addEventListener('click', e => {
e.currentTarget
// ^? (property) Event.currentTarget: EventTarget | null
// #myButtonのcurrentTargetはbutton要素であるため、型アサーションを使って補足する
const button = e.currentTarget as HTMLButtonElement;
});
- 【変数の型にnullが含まれているが、文脈上それが有りえない場合】
このような場合も型について、実装者の方が把握しているため型アサーションを使って型を補足する
const elNull = document.getElementById('foo');
// ^? const elNull: HTMLElement | null
const el = document.getElementById('foo') as HTMLElement;
// ^? const el: HTMLElement
また、非nullアサーションを使ってnull
を取り除く方法もある
const el = document.getElementById('foo')!;
// ^? const el: HTMLElement
- 【外部ライブラリの型が不完全な場合】
外部APIの戻り値の型が提要されていない場合や、不明な場合は型アサーションを使って型を補足する必要がある。
その際に利用する型は、仕様書や外部APIのOpenAPI定義から型を生成したものを可能な限り利用するようにする。
まとめ
- 型アサーションでの型補完は最終手段であることを忘れない
- 型アサーションは、型システムが認識している型を上書きする行為であるため、本当に実装者が型システムより型に詳しい場合のみにする