🦓

TypeScriptでよく見る型エラー

に公開

概要

この記事では、TypeScriptで発生する型エラーの一般的な原因と解決方法についてまとめてみました。
特に、ZodスキーマとTypeScript型定義の間の不整合によるエラーに焦点を当てます。

問題の例

エラーメッセージ

Type '{ id: undefined; propertyId: undefined; price: null; date: null; }' is not assignable to type '{ id?: number | undefined; propertyId?: number | undefined; price?: number | undefined; date?: string[] | null | undefined; }'.

Types of property 'price' are incompatible.
  Type 'null' is not assignable to type 'number | undefined'.

問題の原因

このエラーは以下の要因によって発生します:

  1. スキーマと型定義の不整合
  2. null vs undefined の混在
  3. デフォルト値の型不一致
  4. 配列型と単一型の混同

一般的な解決アプローチ

1. 原因の特定

エラーメッセージの解析

  • エラーメッセージは具体的なプロパティ名と期待される型を示します
  • 例:Types of property 'price' are incompatible

スキーマと型定義の確認

// 問題のある例
// スキーマ定義
const schema = z.object({
  price: z.number().optional(), // number | undefined
  date: z.string().nullable().optional() // string | null | undefined
})

// 型定義
type MyType = {
  price?: number | null, // 不整合:null が含まれている
  date?: string[] | null // 不整合:配列型になっている
}

2. スキーマと型の整合性確保

Zodスキーマの型生成

// 正しい例
const priceSchema = z.object({
  price: z.number().nullable().optional(),
  date: z.array(z.string()).nullable().optional()
})

// 自動生成される型
type PriceType = z.infer<typeof priceSchema>
// {
//   price?: number | null | undefined
//   date?: string[] | null | undefined
// }

手動型定義との整合性

// 手動で型を定義する場合
type PriceType = {
  price?: number | null
  date?: string[] | null
}

// スキーマと一致させる
const priceSchema = z.object({
  price: z.number().nullable().optional(),
  date: z.array(z.string()).nullable().optional()
})

3. null vs undefined の適切な使い分け

型の違い

// optional() - undefined を許可
z.number().optional() // number | undefined

// nullable() - null を許可
z.number().nullable() // number | null

// nullable().optional() - 両方を許可
z.number().nullable().optional() // number | null | undefined

使用場面の判断

// undefined: 値が設定されていない状態
const userSchema = z.object({
  name: z.string().optional(), // 名前が未設定
  email: z.string().optional() // メールが未設定
})

// null: 意図的に空の値
const priceSchema = z.object({
  regularPrice: z.number().nullable(), // 価格が未定
  discountPrice: z.number().nullable() // 割引なし
})

// 両方: 柔軟性が必要な場合
const configSchema = z.object({
  theme: z.string().nullable().optional(), // テーマ設定なし/未設定
  language: z.string().nullable().optional() // 言語設定なし/未設定
})

4. デフォルト値の型一致

問題のある例

const schema = z.object({
  price: z.number().optional()
})

// エラー:null を number | undefined に代入しようとしている
const defaultValues = {
  price: null // ❌ 型エラー
}

正しい例

// スキーマを修正
const schema = z.object({
  price: z.number().nullable().optional()
})

const defaultValues = {
  price: null // ✅ 型一致
}

5. 配列型の適切な定義

問題のある例

// スキーマ:単一の文字列
const schema = z.object({
  dates: z.string().nullable().optional() // string | null | undefined
})

// 型定義:配列
type MyType = {
  dates?: string[] | null // 不整合
}

// 使用箇所:配列として使用
const dates = watch('dates') as string[] // 型アサーションが必要

正しい例

// スキーマ:配列の文字列
const schema = z.object({
  dates: z.array(z.string()).nullable().optional() // string[] | null | undefined
})

// 型定義:配列
type MyType = {
  dates?: string[] | null // 整合
}

// 使用箇所:自然に配列として使用
const dates = watch('dates') || [] // 型アサーション不要

実装チェックリスト

1. スキーマ定義の確認

  • 各フィールドの型が正しく定義されているか
  • optional()nullable() の使い分けが適切か
  • 配列型が必要な場合は z.array() を使用しているか

2. 型定義の整合性

  • 手動型定義がある場合、スキーマと一致しているか
  • z.infer<typeof schema> を使用して自動生成しているか
  • 型の組み合わせ(| null | undefined)が正しいか

3. デフォルト値の確認

  • デフォルト値の型がスキーマと一致しているか
  • nullundefined の使い分けが適切か
  • 配列のデフォルト値が正しく設定されているか

4. 使用箇所の検証

  • コンポーネントでの型アサーションが不要になっているか
  • 配列メソッド(.length, .map() など)が正常に動作するか
  • フォームライブラリとの連携が正常か

よくあるエラーパターン

パターン1: null vs undefined の混在

// エラー
const schema = z.object({ price: z.number().optional() })
const defaults = { price: null } // ❌

// 解決
const schema = z.object({ price: z.number().nullable().optional() })
const defaults = { price: null } // ✅

パターン2: 配列型の不整合

// エラー
const schema = z.object({ items: z.string().optional() })
type Type = { items?: string[] } // ❌

// 解決
const schema = z.object({ items: z.array(z.string()).optional() })
type Type = { items?: string[] } // ✅

パターン3: ネストしたオブジェクトの型不一致

// エラー
const schema = z.object({
  config: z.object({
    theme: z.string().optional()
  }).optional()
})
const defaults = { config: { theme: null } } // ❌

// 解決
const schema = z.object({
  config: z.object({
    theme: z.string().nullable().optional()
  }).optional()
})
const defaults = { config: { theme: null } } // ✅

ベストプラクティス

1. 単一の情報源

// 推奨:スキーマを単一の情報源として使用
const schema = z.object({...})
type MyType = z.infer<typeof schema>

// 避ける:手動で型を定義
type MyType = {...} // スキーマと同期が取れなくなる可能性

2. 一貫した命名規則

// 推奨:明確な命名
const userCreateSchema = z.object({...})
const userUpdateSchema = z.object({...})
const userResponseSchema = z.object({...})

// 避ける:曖昧な命名
const schema = z.object({...})
const userSchema = z.object({...})

3. 段階的な型定義

// 推奨:段階的に型を定義
const baseSchema = z.object({
  id: z.number(),
  name: z.string()
})

const createSchema = baseSchema.omit({ id: true })
const updateSchema = baseSchema.partial()
const responseSchema = baseSchema.extend({
  createdAt: z.date()
})

まとめ

簡単ですが、
TypeScript型エラーの解決は、以下の手順より、型安全性を保ちながら、保守性の高いコードを実現できます。

  1. エラーメッセージの詳細な解析
  2. スキーマと型定義の整合性確認
  3. null vs undefined の適切な使い分け
  4. デフォルト値の型一致
  5. 実際の使用箇所での動作確認

誰かの参考になれば嬉しいです。

Discussion