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));
}
}
}ベストプラクティス
- 可能な限り判別ユニオンを優先。
- 型ガード関数をシンプルに。
- アプリ境界でアサーション関数を使用。
- else ケースを常に処理。
- ユニオン型で Array.isArray() を使用。
- 複数の型ガードを組み合わせ。
- カスタム型ガードを徹底テスト。
結論
TypeScript 型ガードはコンパイル時の型安全性とランタイムの動作のギャップを埋める必須ツールです。これらのパターンをマスターして、型安全で堅牢なコードを書きましょう。
FAQ
型述語とアサーション関数の違いは?
型述語は boolean を返し、アサーション関数はエラーをスローして型を絞り込みます。
判別ユニオンとカスタム型ガードの使い分けは?
型を制御できる場合は判別ユニオン。外部データには型ガード。
型ガードはランタイムエラーを起こす?
カスタム型ガードのロジックが不正確な場合、エラーが発生する可能性があります。
配列内の型をどう絞り込む?
Array.isArray() と型述語付き .filter() を使用します。