⛰️

BiomeとTypeScriptで`any`型を完全に排除する実践ガイド

に公開

TypeScriptでany型を使うことは、型システムの恩恵を放棄することに等しい。BiomeやTypeScriptの厳格な設定ではany型が禁止されるが、LLMやコーディングエージェントが生成するコードには頻繁にanyが含まれる。本記事では、any型を使わずに型安全なコードを書くための具体的な方法を、初心者から中級者向けに実践的なコード例とともに解説する。Airbnbの調査によれば、TypeScriptの導入によってバグの38%が防げるとされており、これらのテクニックをマスターすることで開発効率を大幅に向上させることができる。

1. 背景と問題:なぜany型は禁止されるのか

BiomeとTypeScriptがany型を禁止する理由は明確だ。any型は型チェックを完全に無効化し、TypeScriptを使う意味を失わせるからである。

Biomeが検出する3つのルール

Biomeはany型に関連する3つの重要なルールを提供している。noExplicitAnyはv1.0.0から推奨ルールとして有効化されており、型注釈における明示的なanyの使用を禁止する。noImplicitAnyLetはv1.4.0から追加されたルールで、型注釈も初期化もない変数宣言を検出する。これはTypeScriptの--noImplicitAnyオプションでは検出されない。noEvolvingTypesはv1.6.3から利用可能で、再代入によってany型に進化する変数を防ぐ。

LLMが生成する典型的な問題コード

LLMやコーディングエージェントは、特定のパターンでany型を生成しやすい。API レスポンスの型定義ではdata: anyと記述し、イベントハンドラではfunction handleClick(event: any)とし、サードパーティライブラリの使用時にはconst lib: any = require('lib')と記述する傾向がある。これらはすべて型安全性を損なう危険なパターンである。

具体的なエラーメッセージ

noExplicitAnyルールに違反すると、次のようなエラーが表示される:

code-block.ts:1:15 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━
⚠ Unexpected any. Specify a different type.

> 1 │ let variable: any = 1;
    │               ^^^
  2 │

ℹ any disables many type checking rules. Its use should be avoided.

noImplicitAnyLetでは、初期化されていない変数に対してThis variable implicitly has the any typeというエラーが表示される。

2. any型を回避する具体的なパターンと手順

any型を避けるための基本的な戦略は、より具体的な型システムの機能を活用することである。

unknown型の活用:安全な代替手段

unknown型はanyの型安全な代替である。anyはすべての操作を許可するが、unknownは型チェックを強制するという決定的な違いがある。

// ❌ any - dangerous code
function parseJSON(jsonString: string): any {
  return JSON.parse(jsonString);
}

const data = parseJSON('{"name":"John"}');
console.log(data.name.toUpperCase()); // No type checking

// ✅ unknown - safe code
function parseJSON(jsonString: string): unknown {
  return JSON.parse(jsonString);
}

const data = parseJSON('{"name":"John"}');

// Type guard ensures safety
if (
  data &&
  typeof data === 'object' &&
  'name' in data &&
  typeof data.name === 'string'
) {
  console.log(data.name.toUpperCase()); // Type safe
}

さらに良いアプローチは、カスタム型ガードを作成することである:

interface User {
  name: string;
  age: number;
}

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'name' in obj &&
    'age' in obj &&
    typeof (obj as Record<string, unknown>).name === 'string' &&
    typeof (obj as Record<string, unknown>).age === 'number'
  );
}

function processUser(data: unknown) {
  if (isUser(data)) {
    console.log(`${data.name} is ${data.age} years old`);
  } else {
    throw new Error('Invalid user data');
  }
}

ジェネリクスで再利用可能な型安全コードを書く

ジェネリクスは、anyを使わずに再利用可能なコードを書くための最も強力なツールである。

// ❌ any - array operation
function getFirstElement(arr: any[]): any {
  return arr[0];
}

const num = getFirstElement([1, 2, 3]); // Type is any

// ✅ Generics - type safe implementation
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const num = getFirstElement([1, 2, 3]); // Type is number
const str = getFirstElement(['a', 'b', 'c']); // Type is string

API レスポンスの型定義では、ジェネリクスを使った汎用インターフェースが効果的である:

interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const response = await fetch('/api/users');
  return response.json();
}

async function fetchProducts(): Promise<ApiResponse<Product[]>> {
  const response = await fetch('/api/products');
  return response.json();
}

型ガードとType Narrowingのテクニック

型ガードは、条件分岐内で型を絞り込む機能である。TypeScriptには複数の型ガードの方法がある。

typeof型ガードは、プリミティブ型の判定に使用する:

function padLeft(padding: number | string, input: string): string {
  if (typeof padding === 'number') {
    return ' '.repeat(padding) + input;
  }
  return padding + input;
}

instanceof型ガードは、クラスのインスタンスを判定する:

class Dog {
  bark() {
    console.log('Woof!');
  }
}

class Cat {
  meow() {
    console.log('Meow!');
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

判別可能なユニオン型は、最も強力なパターンである:

interface Circle {
  kind: 'circle';
  radius: number;
}

interface Square {
  kind: 'square';
  sideLength: number;
}

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

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.sideLength ** 2;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

ユーティリティ型で既存の型を変換する

TypeScriptの組み込みユーティリティ型は、anyを使わずに柔軟な型定義を作成できる

**Partial<Type>**はすべてのプロパティをオプショナルにする:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

function updateUser(userId: number, updates: Partial<User>) {
  // updates can have any subset of User's properties
}

updateUser(1, { name: 'John' }); // ✅
updateUser(1, { email: 'john@test.com' }); // ✅
updateUser(1, { invalid: 'value' }); // ❌ Error

**Pick<Type, Keys>**は特定のプロパティだけを選択する:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type UserPublic = Pick<User, 'id' | 'name' | 'email'>;

function displayUser(user: UserPublic) {
  console.log(`${user.name} (${user.email})`);
  // user.password is not accessible
}

**Record<Keys, Type>**は、キーと値の型を指定したオブジェクトを作成する:

type Role = 'admin' | 'user' | 'guest';
type Permission = 'read' | 'write' | 'delete';

const userRoles: Record<Role, Permission[]> = {
  admin: ['read', 'write', 'delete'],
  user: ['read'],
  guest: ['read'],
};

複数のユーティリティ型を組み合わせることで、複雑な型定義も実現できる:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: string;
}

// User registration type (excludes id and role, password is required)
type UserRegistration = Omit<User, 'id' | 'role'> & Required<Pick<User, 'password'>>;

// User update type (id is required, others are optional)
type UserUpdate = Pick<User, 'id'> & Partial<Omit<User, 'id'>>;

型アサーションの適切な使い方

型アサーションは慎重に使用すべきだが、適切な場面では有用である。

DOM要素の型指定は、型アサーションの正当なユースケースである:

const input = document.getElementById('email-input') as HTMLInputElement;
input.value = 'john@example.com';

// Or combine with null check and instanceof
const button = document.querySelector('button');
if (button instanceof HTMLButtonElement) {
  button.disabled = true;
}

型ガードの方が安全であることを常に念頭に置く:

// ❌ Using type assertion
function processValue(value: unknown) {
  const str = value as string;
  return str.toUpperCase();
}

// ✅ Using type guard
function processValue(value: unknown) {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  throw new Error('Value must be a string');
}

3. 実践的なコード例:Before/After

API レスポンスの型定義

// ❌ BEFORE: Dangerous implementation using any
async function fetchUser(id: number) {
  const response = await fetch(`/api/users/${id}`);
  const data: any = await response.json();
  return data;
}

// ✅ AFTER: Type-safe implementation
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const result: ApiResponse<User> = await response.json();
  
  if (!result.success) {
    throw new Error(result.message || 'Failed to fetch user');
  }
  
  return result.data;
}

// ✅ BETTER: With runtime validation
function isUser(obj: unknown): obj is User {
  if (typeof obj !== 'object' || obj === null) {
    return false;
  }
  
  const record = obj as Record<string, unknown>;
  
  return (
    typeof record.id === 'number' &&
    typeof record.name === 'string' &&
    typeof record.email === 'string'
  );
}

async function fetchUserSafe(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();
  
  if (isUser(data)) {
    return data;
  }
  
  throw new Error('Invalid user data from API');
}

イベントハンドラの型定義

// ❌ BEFORE: any type in event handler
function handleClick(event: any) {
  console.log(event.target.value);
}

// ✅ AFTER: Proper type specification
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
  console.log(event.currentTarget.textContent);
}

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value);
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  // Process form data
}

LocalStorageでの型安全な実装

// ❌ BEFORE: Implementation using any
function saveUser(user: any) {
  localStorage.setItem('user', JSON.stringify(user));
}

function getUser(): any {
  const data = localStorage.getItem('user');
  return data ? JSON.parse(data) : null;
}

// ✅ AFTER: Type-safe implementation
interface User {
  id: number;
  name: string;
  email: string;
}

function saveUser(user: User): void {
  localStorage.setItem('user', JSON.stringify(user));
}

function getUser(): User | null {
  const data = localStorage.getItem('user');
  if (!data) return null;
  
  try {
    const parsed: unknown = JSON.parse(data);
    if (isUser(parsed)) {
      return parsed;
    }
  } catch (error) {
    console.error('Failed to parse user data');
  }
  
  return null;
}

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

Reducerパターンの型定義

// ❌ BEFORE: Implementation using any
function reducer(state: any, action: any) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    default:
      return state;
  }
}

// ✅ AFTER: Using discriminated union types
interface State {
  count: number;
  user: string | null;
}

type Action =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'SET_USER'; payload: string }
  | { type: 'CLEAR_USER' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + action.payload };
    case 'DECREMENT':
      return { ...state, count: state.count - action.payload };
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'CLEAR_USER':
      return { ...state, user: null };
    default:
      const _exhaustive: never = action;
      return state;
  }
}

4. 段階的な移行手順

既存のコードベースからany型を排除する際は、段階的なアプローチが効果的である。

小規模プロジェクト(1万行未満)の移行戦略

小規模プロジェクトでは、一括変換が最も効率的である。まずTypeScriptをインストールし、tsconfig.jsonを作成する。初期設定では"strict": trueを有効にし、すべてのファイルを一度に.tsまたは.tsxに変換する。エラーが発生したら即座に修正し、型定義を追加していく。

中規模プロジェクト(1万~10万行)の段階的移行

中規模プロジェクトでは、ハイブリッドアプローチを採用する。Phase 1では基本的なチェックを有効化する:

{
  "compilerOptions": {
    "allowJs": true,
    "noImplicitAny": false,
    "strict": false
  }
}

ファイルを一つずつ変換し、リーフモジュール(依存関係のないモジュール)から始める。Phase 2ではnoImplicitAnyを有効化する:

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

これにより、パラメータに明示的な型付けが強制される。Phase 3ではstrictNullChecksを有効化し、nullundefinedを区別する:

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Phase 4で完全なstrict modeを有効化する:

{
  "compilerOptions": {
    "strict": true
  }
}

大規模プロジェクト(10万行以上)の移行

大規模プロジェクトでは、自動化ツールの使用を検討すべきである。Airbnbのts-migrateは、6万行以上のコードを一日で変換できる:

npx ts-migrate-init frontend/src
npx ts-migrate-rename frontend/src
npx ts-migrate-migrate frontend/src

ts-migrateは、.jsファイルを.tsに変換し、コンパイルエラーに@ts-expect-errorコメントを追加し、any型を自動的に挿入する。その後、段階的にany型を適切な型に置き換えていく。

一時的なマーカーの使用

移行中は、$TSFixMeのような型エイリアスを使用して、後で修正すべき箇所をマークする:

type $TSFixMe = any;
type $TSFixMeFunction = (...args: any[]) => any;

// Temporary usage
function legacyFunction(data: $TSFixMe): $TSFixMeFunction {
  // TODO: Define types
  return (args: $TSFixMe) => data.process(args);
}

後でコードベース全体を検索し、これらのマーカーを順次削除していく。

サードパーティライブラリの型定義作成

型定義が存在しないライブラリに対しては、.d.tsファイルを作成する:

// types/third-party-lib/index.d.ts
declare module 'third-party-library-name' {
  export interface Config {
    apiKey: string;
    timeout?: number;
  }
  
  export function initialize(config: Config): void;
  export function doSomething(data: string): Promise<Result>;
  
  export interface Result {
    success: boolean;
    data: unknown; // Can be improved gradually
  }
}

tsconfig.jsonで型定義の場所を指定する:

{
  "compilerOptions": {
    "typeRoots": [
      "./types",
      "./node_modules/@types"
    ]
  }
}

5. ツールと設定

適切なツールと設定は、any型の使用を防ぐための重要な要素である。

Biomeの設定

Biomeでany型を厳格に禁止するための設定例:

{
  "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "files": {
    "ignore": ["dist/**", "build/**", "node_modules/**"]
  },
  "formatter": {
    "enabled": true,
    "indentWidth": 2,
    "indentStyle": "space"
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "error",
        "noImplicitAnyLet": "error",
        "noEvolvingTypes": "error"
      },
      "style": {
        "useImportType": "error",
        "useExportType": "error"
      }
    }
  },
  "overrides": [
    {
      "include": ["tests/**", "**/*.test.ts"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off"
          }
        }
      }
    }
  ]
}

overrides機能を使用すると、テストファイルなど特定のファイルでのみルールを緩和できる。これにより、本番コードは厳格に保ちながら、テストコードでは柔軟性を持たせることができる。

TypeScriptコンパイラオプション

本番環境用の厳格なtsconfig.json設定:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "jsx": "react",
    
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "useUnknownInCatchVariables": true,
    
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    
    "moduleResolution": "node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "skipLibCheck": true,
    
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

strict: trueは8つ以上の厳格なチェックを一括で有効化する最も重要なオプションである。

ESLintとの併用

BiomeとESLintを併用する場合、または移行途中でESLintを使用している場合の設定:

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: './tsconfig.json',
    ecmaVersion: 2020,
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking'
  ],
  rules: {
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'warn',
    '@typescript-eslint/no-unsafe-member-access': 'warn',
    '@typescript-eslint/no-unsafe-call': 'warn',
    '@typescript-eslint/no-unsafe-return': 'warn',
    '@typescript-eslint/explicit-function-return-type': 'warn',
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/await-thenable': 'error',
    '@typescript-eslint/no-unused-vars': ['error', {
      argsIgnorePattern: '^_'
    }]
  }
};

開発環境の設定

VS Code で Biome を使用する場合の.vscode/settings.json設定:

{
  "biome.enabled": true,
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "always",
    "source.organizeImports.biome": "always"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}

移行支援ツール

**ts-migrate(Airbnb製)**は、JavaScriptからTypeScriptへの自動移行をサポートする:

npm install -g ts-migrate
npx ts-migrate-init frontend/src
npx ts-migrate-rename frontend/src
npx ts-migrate-migrate frontend/src

このツールは、ファイルをリネームし、コンパイルエラーに@ts-expect-errorを追加し、PropTypesをTypeScript型に変換する。ただし、生成されるコードには多くのany型が含まれるため、後で手動で改善する必要がある

6. トラブルシューティング

any型を削除する過程で遭遇する一般的なエラーと解決方法を紹介する。

エラー:「Parameter 'x' implicitly has an 'any' type」

このエラーは、noImplicitAnyが有効な場合に、型注釈のないパラメータに対して発生する。

// ❌ Error
function greet(name) {
  return `Hello ${name}`;
}

// ✅ Solution 1: Add type annotation
function greet(name: string) {
  return `Hello ${name}`;
}

// ✅ Solution 2: Infer type from default value
function greet(name = "World") {
  return `Hello ${name}`;
}

エラー:「Object is possibly 'null' or 'undefined'」

strictNullChecksが有効な場合、nullの可能性がある値にアクセスすると発生する。

// ❌ Error
function getLength(str: string | null) {
  return str.length; // Error!
}

// ✅ Solution 1: Type guard
function getLength(str: string | null) {
  if (str !== null) {
    return str.length;
  }
  return 0;
}

// ✅ Solution 2: Optional chaining
function getLength(str: string | null) {
  return str?.length ?? 0;
}

// ✅ Solution 3: Non-null assertion (use with caution)
function getLength(str: string | null) {
  return str!.length; // Guarantees str is not null
}

エラー:「Property 'X' does not exist on type 'Y'」

厳格な型付け後、未定義のプロパティにアクセスしようとすると発生する。

// ❌ Error
const obj = {};
obj.name = "John"; // Error!

// ✅ Solution 1: Define interface
interface Person {
  name: string;
}
const obj: Person = { name: "John" };

// ✅ Solution 2: Index signature
const obj: { [key: string]: string } = {};
obj.name = "John";

// ✅ Solution 3: Record utility type
const obj: Record<string, string> = {};
obj.name = "John";

エラー:「Type 'X' is not assignable to type 'Y'」

型の不一致が発生した場合の対処法。

// ❌ Error
let foo: string = "hello";
foo = 123; // Error!

// ✅ Solution 1: Union type
let foo: string | number = "hello";
foo = 123; // OK

// ✅ Solution 2: Generics
function identity<T>(arg: T): T {
  return arg;
}

型推論の問題:Array.filter()

Array.filter()で型が正しく絞り込まれない場合がある。

// ❌ Type not narrowed correctly
const data = ['one', null, 'two'];
data
  .filter(Boolean)
  .forEach((x: string) => console.log(x)); // Error!

// ✅ Use custom type guard
function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

data
  .filter(isNotNull)
  .forEach((x: string) => console.log(x)); // Works!

型推論の問題:オブジェクトプロパティの拡大

オブジェクトのプロパティ型が広すぎる場合の対処法。

// ❌ Problem
const config = {
  method: "GET" // Type is string
};

function request(method: "GET" | "POST") {
  // ...
}
request(config.method); // Error: string is not assignable to "GET" | "POST"

// ✅ Solution 1: const assertion
const config = {
  method: "GET"
} as const;

// ✅ Solution 2: Explicit type
const config: { method: "GET" | "POST" } = {
  method: "GET"
};

エラー:「Cannot find module 'X'」

型定義が見つからない場合の対処法。

// ✅ Solution 1: Install type definitions
// npm install --save-dev @types/lodash

// ✅ Solution 2: Create declaration file
// types/module-name/index.d.ts
declare module 'module-name';

// ✅ Solution 3: Declare inline
declare module 'module-name' {
  export function someFunc(): void;
}

結論:型安全性への投資が生産性を向上させる

any型を排除することは、短期的には追加の労力を要するが、長期的には大きなリターンをもたらす。Airbnbの調査では、TypeScriptによって38%のバグが防げることが判明している。また、型システムは自己文書化の役割を果たし、新しい開発者のオンボーディング時間を30-50%短縮する。

本記事で紹介したテクニックの核心は以下の通りである。any型の代わりにunknownを使用し、型ガードで安全性を確保する。ジェネリクスを活用して再利用可能なコードを書く。ユーティリティ型で既存の型を柔軟に変換する。判別可能なユニオン型で複雑な型を表現する。段階的な移行戦略を採用し、プロジェクトの規模に応じて適切なツールを選択する。

BiomeとTypeScriptの厳格な設定は、初めは制約に感じるかもしれない。しかし、これらの制約こそが、バグを未然に防ぎ、リファクタリングを安全にし、コードの意図を明確にする力となる。型安全性は制約ではなく、自由をもたらす基盤である。今日からany型を排除し、TypeScriptの真の力を解き放とう。

Discussion