型アサーションの正しい使い方

TypeScript

2025-05-18

はじめに

これまで、なんとなくの雰囲気でTypeScriptを書いており、TypeScriptらしいコードや型システムの活用について深く意識することなく開発をしていました。

もっとTypeScriptで良いコードが書けるようになりたいと思い、『Effective TypeScript 第2版』を読みはじめ、その中でこれまで曖昧に理解していたことが明確になりました。

特に型アサーションとの向き合い方は大きな学びがあったため、本記事では、その気づきや知見を整理してまとめたいと思います。

型の活用について

まず、TypeScriptで型を扱うときには、次の優先順位を意識したいです。

  1. 型推論:型システムを活用して、自動的に型を導き出す。まずはこれを最大限活用
  2. 型アノテーション:明示的に型を書く必要がある場合に補足する
  3. 型アサーション:値の型を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;

Playground

また、型アノテーションを使うと、余剰プロパティチェックという存在しないプロパティ使用時に型エラーとするチェッカーが効かなくなります。

// 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;

Playground

関数の戻り値の型を型アサーションで補足している

外部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>
}

Playground

このような場合は関数本体に型アサーションを追加し、型アサーションが関数の実装に隠蔽されるようにすることで、呼び出し元での型アサーションが不要になり、型アサーションの利用が局所的になります。

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}`)
}

Playground

型アサーションを使うべき場面

値の型をTypeScript以上に、実装者が本当に把握しているとき

TypeScriptはDOM要素にアクセスできないため、実装者の方が型を把握しているケースが多いので問題ない。

document.querySelector('#myButton')?.addEventListener('click', e => {
   e.currentTarget
   // ^? (property) Event.currentTarget: EventTarget | null
   
   // #myButtonのcurrentTargetはbutton要素であるため、型アサーションを使って補足する
   const button = e.currentTarget as HTMLButtonElement;
 });

Playground

このような場合も型について、実装者の方が把握しているため型アサーションを使って型を補足する

const elNull = document.getElementById('foo');
//    ^? const elNull: HTMLElement | null
const el = document.getElementById('foo') as HTMLElement;
//    ^? const el: HTMLElement

Playground

また、非nullアサーションを使ってnullを取り除く方法もある

const el = document.getElementById('foo')!;
//    ^? const el: HTMLElement

Playground

外部APIの戻り値の型が提要されていない場合や、不明な場合は型アサーションを使って型を補足する必要がある。

その際に利用する型は、仕様書や外部APIのOpenAPI定義から型を生成したものを可能な限り利用するようにする。

まとめ