DevToolBoxFREE
BlogAdvertise

TypeScript 型ガード:ランタイム型チェック完全ガイド

13分by DevToolBox

TypeScript の型システムは強力ですが、型はランタイムで消去されます。型ガードはこのギャップを埋め、ランタイムで型を絞り込みながらコンパイル時の型安全性を維持します。

型ガードとは?

型ガードは条件ブロック内で変数の型を絞り込むランタイムチェックです。

// Without type guard — TypeScript only knows 'value' is string | number
function process(value: string | number) {
  // value.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'

  // With type guard — TypeScript narrows the type
  if (typeof value === 'string') {
    console.log(value.toUpperCase()); // OK: value is string here
  } else {
    console.log(value.toFixed(2));    // OK: value is number here
  }
}

typeof 型ガード

typeof 演算子は最もシンプルな型ガードです。

function formatValue(value: string | number | boolean): string {
  if (typeof value === 'string') {
    // TypeScript knows: value is string
    return value.trim().toLowerCase();
  }
  if (typeof value === 'number') {
    // TypeScript knows: value is number
    return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
  }
  // TypeScript knows: value is boolean
  return value ? 'Yes' : 'No';
}

// typeof works for these primitive types:
// 'string' | 'number' | 'bigint' | 'boolean'
// 'symbol' | 'undefined' | 'object' | 'function'

// Caveat: typeof null === 'object'
function processNullable(value: string | null) {
  if (typeof value === 'object') {
    // value could be null here!  typeof null === 'object'
  }
  if (value !== null && typeof value === 'object') {
    // Safe: value is not null
  }
}

instanceof 型ガード

instanceof はオブジェクトがクラスのインスタンスかチェックします。

class HttpError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'HttpError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function handleError(error: Error) {
  if (error instanceof HttpError) {
    // TypeScript knows: error is HttpError
    console.log(`HTTP ${error.statusCode}: ${error.message}`);
    if (error.statusCode === 401) {
      redirectToLogin();
    }
  } else if (error instanceof ValidationError) {
    // TypeScript knows: error is ValidationError
    console.log(`Validation failed on field: ${error.field}`);
    highlightField(error.field);
  } else {
    // TypeScript knows: error is Error
    console.log(`Unexpected error: ${error.message}`);
  }
}

// instanceof with Date
function formatDate(input: string | Date): string {
  if (input instanceof Date) {
    return input.toISOString();
  }
  return new Date(input).toISOString();
}

"in" 演算子型ガード

in 演算子はプロパティの存在をチェックします。

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ('fly' in animal) {
    // TypeScript knows: animal is Bird
    animal.fly();
  } else {
    // TypeScript knows: animal is Fish
    animal.swim();
  }
}

// Practical example: API responses
interface SuccessResponse {
  data: unknown;
  status: 'ok';
}

interface ErrorResponse {
  error: string;
  code: number;
}

function handleResponse(res: SuccessResponse | ErrorResponse) {
  if ('error' in res) {
    // TypeScript knows: res is ErrorResponse
    console.error(`Error ${res.code}: ${res.error}`);
  } else {
    // TypeScript knows: res is SuccessResponse
    processData(res.data);
  }
}

カスタム型ガード関数

型述語を返す関数で複雑なチェックをカプセル化します。

// Type predicate syntax: paramName is Type
interface User {
  id: string;
  name: string;
  email: string;
}

interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// Custom type guard function
function isAdmin(user: User): user is Admin {
  return 'role' in user && (user as Admin).role === 'admin';
}

function showDashboard(user: User) {
  if (isAdmin(user)) {
    // TypeScript knows: user is Admin
    console.log(`Admin: ${user.name}, Permissions: ${user.permissions.join(', ')}`);
    renderAdminPanel(user.permissions);
  } else {
    // TypeScript knows: user is User (not Admin)
    console.log(`User: ${user.name}`);
    renderUserDashboard();
  }
}

// Type guard for checking non-null
function isNotNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// Use with .filter() to narrow array types
const items: (string | null)[] = ['hello', null, 'world', null];
const strings: string[] = items.filter(isNotNull);
// strings is string[] — no more nulls!

// Type guard for object shapes
function isValidUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as User).id === 'string' &&
    typeof (data as User).name === 'string' &&
    typeof (data as User).email === 'string'
  );
}

判別ユニオン

共通のリテラルプロパティでユニオンメンバーを区別します。

// The discriminant property: 'type'
interface Circle {
  type: 'circle';
  radius: number;
}

interface Rectangle {
  type: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  type: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      // TypeScript knows: shape is Circle
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // TypeScript knows: shape is Rectangle
      return shape.width * shape.height;
    case 'triangle':
      // TypeScript knows: shape is Triangle
      return (shape.base * shape.height) / 2;
  }
}

// Real-world: Redux actions
type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; error: string }
  | { type: 'SET_FILTER'; filter: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      // TypeScript knows: action.payload exists and is User[]
      return { ...state, loading: false, users: action.payload };
    case 'FETCH_ERROR':
      // TypeScript knows: action.error exists and is string
      return { ...state, loading: false, error: action.error };
    case 'SET_FILTER':
      return { ...state, filter: action.filter };
  }
}

アサーション関数

アサーション失敗時にエラーをスローして型を絞り込みます。

// Assertion function syntax: asserts paramName is Type
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`);
  }
}

// Usage: type narrows AFTER the assertion call
function processInput(input: unknown) {
  assertIsString(input);
  // TypeScript knows: input is string from here on
  console.log(input.toUpperCase());
}

// Assert non-null
function assertDefined<T>(
  value: T | null | undefined,
  name: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(`${name} must be defined`);
  }
}

// Practical: Form validation
interface FormData {
  name: unknown;
  email: unknown;
  age: unknown;
}

function assertValidForm(data: FormData): asserts data is {
  name: string;
  email: string;
  age: number;
} {
  if (typeof data.name !== 'string' || data.name.length === 0) {
    throw new ValidationError('name', 'Name is required');
  }
  if (typeof data.email !== 'string' || !data.email.includes('@')) {
    throw new ValidationError('email', 'Valid email is required');
  }
  if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
    throw new ValidationError('age', 'Valid age is required');
  }
}

function handleSubmit(data: FormData) {
  assertValidForm(data);
  // TypeScript knows all fields are properly typed here
  console.log(`Name: ${data.name}, Email: ${data.email}, Age: ${data.age}`);
}

制御フロー型絞り込み

TypeScript は制御フロー分析で型を絞り込みます。

真偽値の絞り込み

真偽値チェックに基づいて型を絞り込みます。

function greet(name: string | null | undefined) {
  if (name) {
    // TypeScript knows: name is string (not null/undefined)
    console.log(`Hello, ${name.toUpperCase()}!`);
  }
}

// Truthy narrowing with arrays
function processItems(items?: string[]) {
  if (items && items.length > 0) {
    // TypeScript knows: items is string[] (defined and non-empty)
    const first = items[0]; // string
  }
}

// Logical operators for narrowing
function getValue(a: string | null, b: string | null): string {
  // || narrows: returns first truthy value
  return a || b || 'default';
}

// Nullish coalescing for narrowing
function getConfig(config?: { timeout: number }) {
  const timeout = config?.timeout ?? 3000;
  // timeout is number (never undefined)
}

等価性の絞り込み

値の比較で型を絞り込みます。

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // TypeScript knows: both x and y are string
    // (the only common type)
    console.log(x.toUpperCase());
    console.log(y.toUpperCase());
  }
}

// Equality narrowing with null
function process(value: string | null) {
  if (value !== null) {
    // TypeScript knows: value is string
    console.log(value.length);
  }
}

// Switch statement narrowing
function handleStatus(status: 'loading' | 'success' | 'error') {
  switch (status) {
    case 'loading':
      showSpinner();
      break;
    case 'success':
      hideSpinner();
      break;
    case 'error':
      showError();
      break;
  }
}

高度な型ガードパターン

never による網羅チェック

never 型ですべてのケースの処理を保証します。

type Shape = Circle | Rectangle | Triangle;

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

function getArea(shape: Shape): number {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      // If you add a new Shape variant and forget to handle it,
      // TypeScript will error here because shape won't be 'never'
      return assertNever(shape);
  }
}

// Alternative: satisfies + exhaustive check
function describeShape(shape: Shape): string {
  switch (shape.type) {
    case 'circle':    return `Circle with radius ${shape.radius}`;
    case 'rectangle': return `${shape.width}x${shape.height} rectangle`;
    case 'triangle':  return `Triangle with base ${shape.base}`;
    default: {
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

ジェネリック型ガード

任意の型で動作するジェネリック型ガードを作成できます。

// Generic type guard for checking object keys
function hasProperty<K extends string>(
  obj: unknown,
  key: K
): obj is Record<K, unknown> {
  return typeof obj === 'object' && obj !== null && key in obj;
}

// Usage
function processData(data: unknown) {
  if (hasProperty(data, 'name') && hasProperty(data, 'age')) {
    console.log(data.name, data.age); // both are 'unknown' but accessible
  }
}

// Generic type guard for arrays
function isArrayOf<T>(
  arr: unknown,
  guard: (item: unknown) => item is T
): arr is T[] {
  return Array.isArray(arr) && arr.every(guard);
}

const isString = (value: unknown): value is string =>
  typeof value === 'string';

function handleInput(data: unknown) {
  if (isArrayOf(data, isString)) {
    // data is string[]
    data.forEach(s => console.log(s.toUpperCase()));
  }
}

配列型ガード

Array.isArray() と型安全な filter パターンを使用します。

// Filtering with type guards
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2, null];

// Filter nulls with type predicate
const nonNull = mixed.filter(
  (item): item is string | number => item !== null
);
// nonNull: (string | number)[]

// Filter to specific type
const stringsOnly = mixed.filter(
  (item): item is string => typeof item === 'string'
);
// stringsOnly: string[]

// Type-safe .find()
const found = mixed.find(
  (item): item is string => typeof item === 'string'
);
// found: string | undefined

// Array.isArray() type guard
function flatten(input: string | string[]): string[] {
  if (Array.isArray(input)) {
    return input; // string[]
  }
  return [input]; // wrap single string in array
}

実践例

API レスポンス処理

API レスポンスのランタイム検証に不可欠です。

// API response types
interface ApiSuccess<T> {
  success: true;
  data: T;
}

interface ApiError {
  success: false;
  error: { message: string; code: string };
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// Type guard based on discriminant
function isApiSuccess<T>(res: ApiResponse<T>): res is ApiSuccess<T> {
  return res.success === true;
}

// Usage with fetch
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json: ApiResponse<User> = await response.json();

  if (isApiSuccess(json)) {
    return json.data; // TypeScript knows: json.data is User
  } else {
    throw new Error(json.error.message);
  }
}

// Validate unknown API data
function isUser(data: unknown): data is User {
  if (typeof data !== 'object' || data === null) return false;
  const obj = data as Record<string, unknown>;
  return (
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string'
  );
}

イベント処理

DOM イベント型を絞り込みます。

function handleEvent(event: Event) {
  if (event instanceof MouseEvent) {
    console.log(`Mouse at (${event.clientX}, ${event.clientY})`);
  } else if (event instanceof KeyboardEvent) {
    console.log(`Key pressed: ${event.key}`);
  } else if (event instanceof TouchEvent) {
    console.log(`Touch points: ${event.touches.length}`);
  }
}

// Input element type narrowing
function handleInput(event: Event) {
  const target = event.target;
  if (target instanceof HTMLInputElement) {
    console.log(target.value); // string
  } else if (target instanceof HTMLSelectElement) {
    console.log(target.selectedIndex); // number
  }
}

エラー処理

異なるエラー型を型安全に処理します。

// Type guard for unknown caught errors
function isError(error: unknown): error is Error {
  return error instanceof Error;
}

function getErrorMessage(error: unknown): string {
  if (isError(error)) return error.message;
  if (typeof error === 'string') return error;
  return 'An unknown error occurred';
}

// Safe error handling in async code
async function safeFetch(url: string) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new HttpError(res.status, res.statusText);
    return await res.json();
  } catch (error) {
    if (error instanceof HttpError) {
      handleHttpError(error); // error.statusCode available
    } else if (error instanceof TypeError) {
      handleNetworkError(error); // Network failure
    } else {
      handleUnknownError(getErrorMessage(error));
    }
  }
}

ベストプラクティス

  1. 可能な限り判別ユニオンを優先。
  2. 型ガード関数をシンプルに。
  3. アプリ境界でアサーション関数を使用。
  4. else ケースを常に処理。
  5. ユニオン型で Array.isArray() を使用。
  6. 複数の型ガードを組み合わせ。
  7. カスタム型ガードを徹底テスト。

結論

TypeScript 型ガードはコンパイル時の型安全性とランタイムの動作のギャップを埋める必須ツールです。これらのパターンをマスターして、型安全で堅牢なコードを書きましょう。

FAQ

型述語とアサーション関数の違いは?

型述語は boolean を返し、アサーション関数はエラーをスローして型を絞り込みます。

判別ユニオンとカスタム型ガードの使い分けは?

型を制御できる場合は判別ユニオン。外部データには型ガード。

型ガードはランタイムエラーを起こす?

カスタム型ガードのロジックが不正確な場合、エラーが発生する可能性があります。

配列内の型をどう絞り込む?

Array.isArray() と型述語付き .filter() を使用します。

𝕏 Twitterin LinkedIn
この記事は役に立ちましたか?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy