📘

「なんとなく」から卒業!Next.js/Nuxt.jsで使うTypeScriptの型定義パターン完全ガイド

に公開

はじめに

フロントエンド開発で TypeScript を使っているけれど、「なんとなく」型定義を書いている方も多いのではないでしょうか?

この記事では、Next.js/Nuxt.js などのフロントエンド開発で実際によく使う TypeScript のパターンを厳選してまとめました。Utility Types、Mapped Types、配列操作など、実務で毎日使うテクニックを体系的に解説します。

こんな方におすすめ:

  • TypeScript を「なんとなく」使っている
  • Utility Types の使い分けがわからない
  • Mapped Types が難しく感じる
  • フォームバリデーションの型定義で悩む
  • 配列操作で破壊的メソッドを使ってしまう

📚 Utility Types(ユーティリティ型)

TypeScript が標準で提供している便利な型変換ツールです。

Partial<T> - 全プロパティを optional に

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

type PartialUser = Partial<User>
// {
//   name?: string
//   email?: string
//   age?: number
// }

// 使用例
const updateUser = (id: number, updates: Partial<User>) => {
  // 一部のフィールドだけ更新できる
}

updateUser(1, { name: '太郎' })  // ✅ OK
updateUser(2, { email: 'new@example.com' })  // ✅ OK

実務での使い所:

  • フォームの更新(一部だけ変更)
  • API の PATCH リクエスト
  • 設定のオプション

Required<T> - 全プロパティを required に

interface Config {
  apiUrl?: string
  timeout?: number
  debug?: boolean
}

type RequiredConfig = Required<Config>
// {
//   apiUrl: string      // ? が外れた
//   timeout: number
//   debug: boolean
// }

// 使用例
const validateConfig = (config: Required<Config>) => {
  // 全フィールドが必須
}

実務での使い所:

  • 初期化後の設定を保証
  • バリデーション関数

Readonly<T> - 全プロパティを readonly に

interface User {
  id: number
  name: string
}

type ReadonlyUser = Readonly<User>
// {
//   readonly id: number
//   readonly name: string
// }

const user: ReadonlyUser = { id: 1, name: '太郎' }
user.name = '次郎'  // ❌ エラー!

実務での使い所:

  • 定数オブジェクト
  • Immutable データ
  • Props(変更されたくない)

Pick<T, K> - 一部のプロパティだけ取り出す

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

type UserPreview = Pick<User, 'id' | 'name'>
// {
//   id: number
//   name: string
// }

// 使用例
const showUserPreview = (user: UserPreview) => {
  // idとnameだけ表示
}

実務での使い所:

  • API レスポンスの型定義
  • 画面表示用の型
  • パスワードなど機密情報を除外

Omit<T, K> - 一部のプロパティを除外

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

type UserWithoutPassword = Omit<User, 'password'>
// {
//   id: number
//   name: string
//   email: string
// }

// 使用例
const sendToFrontend = (user: UserWithoutPassword) => {
  // パスワード以外を送信
}

実務での使い所:

  • 機密情報の除外
  • API レスポンス
  • フロントエンドへの送信データ

Record<K, V> - キーと値の型を指定

type Status = 'success' | 'error' | 'pending'

type StatusMessages = Record<Status, string>
// {
//   success: string
//   error: string
//   pending: string
// }

const messages: StatusMessages = {
  success: '成功しました',
  error: 'エラーが発生しました',
  pending: '処理中...'
}

// 使用例2: 動的なキー
type UserMap = Record<number, User>  // { [userId: number]: User }

実務での使い所:

  • 定数マップ
  • ステータスメッセージ
  • ID → データのマッピング

ReturnType<T> - 関数の戻り値の型を取得

function getUser() {
  return { id: 1, name: '太郎' }
}

type User = ReturnType<typeof getUser>
// {
//   id: number
//   name: string
// }

// 使用例
const processUser = (user: User) => {
  // getUser() の戻り値と同じ型
}

実務での使い所:

  • 既存関数の型を再利用
  • API レスポンスの型定義

Parameters<T> - 関数の引数の型を取得

function createUser(name: string, age: number) {
  return { name, age }
}

type CreateUserParams = Parameters<typeof createUser>
// [name: string, age: number]

// 使用例
const callCreateUser = (...args: CreateUserParams) => {
  createUser(...args)
}

実務での使い所:

  • 関数のラッパー作成
  • 引数の型を再利用

🗺️ Mapped Types(マップ型)

オブジェクトの各プロパティを変換する高度な型定義です。

基本パターン

type MappedType<T> = {
  [K in keyof T]: 変換後の型
}

例1: ValidationErrors - フォームのエラーメッセージ

interface RegisterForm {
  name: string
  email: string
  password: string
}

type ValidationErrors<T> = {
  [K in keyof T]?: string | undefined
}

// 使用例
const errors: ValidationErrors<RegisterForm> = {}
errors.name = 'ユーザー名が短すぎます'
errors.email = 'メールアドレスの形式が正しくありません'

実務での使い所:

  • フォームのエラーメッセージ
  • バリデーション結果
  • 状態管理

例2: FormValidator - 各プロパティの型を変換

interface RegisterForm {
  name: string
  email: string
  password: string
  age?: number
  agreedToTerms: boolean
}

type FormValidator<T> = Partial<{
  [K in keyof T]: ValidationRule<T[K]>
}>

// 展開すると
type RegisterFormValidator = {
  name?: ValidationRule<string>
  email?: ValidationRule<string>
  password?: ValidationRule<string>
  age?: ValidationRule<number | undefined>
  agreedToTerms?: ValidationRule<boolean>
}

ポイント:

  • [K in keyof T] で各プロパティ名を取り出す
  • T[K] で各プロパティの型を取り出す
  • ValidationRule<T[K]> で型を変換
  • Partial で全体を optional に

実務での使い所:

  • フォームバリデーションルール定義
  • 各プロパティに対して何か処理を定義したいとき

例3: LoadingStates - 各フィールドのローディング状態

interface ApiCalls {
  fetchUser: () => Promise<User>
  fetchProjects: () => Promise<Project[]>
  fetchComments: () => Promise<Comment[]>
}

type LoadingStates<T> = {
  [K in keyof T]?: boolean
}

// 使用例
const loading: LoadingStates<ApiCalls> = {}
loading.fetchUser = true
loading.fetchProjects = false

実務での使い所:

  • API 呼び出しのローディング管理
  • 複数の非同期処理の状態

例4: Nullable - 全プロパティを nullable に

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

type Nullable<T> = {
  [K in keyof T]: T[K] | null
}

type NullableUser = Nullable<User>
// {
//   name: string | null
//   email: string | null
//   age: number | null
// }

実務での使い所:

  • API レスポンス(null 許容)
  • データベースのカラム定義

例5: DeepPartial - ネストしたオブジェクトも全部 optional

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}

interface Config {
  api: {
    url: string
    timeout: number
  }
  ui: {
    theme: string
  }
}

type PartialConfig = DeepPartial<Config>
// {
//   api?: {
//     url?: string
//     timeout?: number
//   }
//   ui?: {
//     theme?: string
//   }
// }

実務での使い所:

  • 設定の部分更新
  • 深いオブジェクトの更新

🔑 keyof(キーオブ)

オブジェクトの全プロパティ名を Union 型で取得します。

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

type UserKeys = keyof User
// 'id' | 'name' | 'email'

// 使用例1: プロパティ名を引数に
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user: User = { id: 1, name: '太郎', email: 'taro@example.com' }
const name = getProperty(user, 'name')  // string型
const id = getProperty(user, 'id')      // number型

// 使用例2: Mapped Types と組み合わせ
type ValidationErrors<T> = {
  [K in keyof T]?: string
  //    ↑ keyof で全プロパティを取得
}

// 使用例3: 関数の引数で型安全に
function getErrorMessage<T>(
  errors: ValidationErrors<T>,
  field: keyof T  // ← Tのプロパティ名だけOK
): string | undefined {
  return errors[field]
}

const errors = { name: 'エラー', email: undefined }
getErrorMessage(errors, 'name')   // ✅ OK
getErrorMessage(errors, 'email')  // ✅ OK
getErrorMessage(errors, 'xxx')    // ❌ コンパイルエラー(xxxは存在しない)

keyof の威力:

  • ✅ 存在しないプロパティ名を弾く(コンパイルエラー)
  • ✅ タイポを検出('password' を 'pasword' と書いたらエラー)
  • ✅ IDE の補完が効く(候補が出る)

実務での使い所:

  • 動的なプロパティアクセス(型安全に)
  • Mapped Types
  • ジェネリクスの制約
  • フォームのフィールド名指定

📊 よく混同するやつの違い

Mapped Types vs Index Signature

// ❌ Index Signature(インデックスシグネチャ)
type AnyKeys = {
  [key: string]: string  // どんなキーでもOK
}

const obj1: AnyKeys = {
  foo: 'bar',
  hoge: 'fuga',
  anything: 'ok'  // ← なんでもOK
}

// ✅ Mapped Types(マップ型)
type SpecificKeys<T> = {
  [K in keyof T]: string  // Tのキーだけ
}

interface User {
  name: string
  email: string
}

const obj2: SpecificKeys<User> = {
  name: 'foo',
  email: 'bar',
  // anything: 'ng'  // ❌ エラー!Userにないキー
}

違い:

  • Index Signature: 任意のキーを受け入れる
  • Mapped Types: 元の型のキーだけ受け入れる

Type Assertion vs Type Annotation

// Type Assertion(型アサーション)
const value = something as string
const value2 = <string>something  // 古い書き方

// Type Annotation(型注釈)
const value: string = something

違い:

  • Assertion: 「これはこの型だよ」って宣言(強制)
  • Annotation: 「この変数はこの型」って指定(チェック)

💡 実務でよく使うパターン集

パターン1: API 型定義

// 単一リソース
interface ApiResponse<T> {
  data: T
}

// 一覧
interface ListResponse<T> {
  data: T[]
  total: number
  page: number
  perPage: number
}

// 関数シグネチャ
function fetchUser(id: number): Promise<ApiResponse<User>>
function fetchUsers(page: number): Promise<ListResponse<User>>

パターン2: フォームバリデーション

// フォーム型
interface RegisterForm {
  name: string
  email: string
  password: string
}

// エラーメッセージ
type ValidationErrors<T> = {
  [K in keyof T]?: string | undefined
}

// バリデーションルール
type ValidationRule<T> = Partial<{
  required: boolean
  minLength: number
  maxLength: number
  pattern: RegExp
  custom: (value: T) => boolean | string
}>

パターン3: Composable/Hooks

interface UseProjectListReturn {
  projects: Ref<Project[]>
  loading: Ref<boolean>
  error: Ref<string | null>
  fetchProjects: () => Promise<void>
  refresh: () => Promise<void>
}

function useProjectList(options?: Options): UseProjectListReturn {
  // ...
}

パターン4: Promise 型

// パターン: async関数は必ずPromiseを返す
async function fetchUser(): Promise<User> {
  const res = await fetch('/api/user')
  return res.json()
}

// パターン: awaitで受け取る時はPromiseが外れる
const user: User = await fetchUser()
//        ↑ Promise<User>じゃなくてUser型

📦 スプレッド構文(...)のよくある使い方

配列のスプレッド

// 1. 配列のコピー
const original = [1, 2, 3]
const copy = [...original]  // [1, 2, 3]
copy.push(4)  // originalは変わらない

// 2. 配列の結合
const arr1 = [1, 2]
const arr2 = [3, 4]
const merged = [...arr1, ...arr2]  // [1, 2, 3, 4]

// 3. 要素の追加(末尾)
const items = [1, 2, 3]
const newItems = [...items, 4, 5]  // [1, 2, 3, 4, 5]

// 4. 要素の追加(先頭)
const newItems2 = [0, ...items]  // [0, 1, 2, 3]

// 5. 要素の削除(filterと組み合わせ)
const filtered = [...items].filter(x => x !== 2)  // [1, 3]

オブジェクトのスプレッド

// 1. オブジェクトのコピー
const original = { name: '太郎', age: 20 }
const copy = { ...original }  // { name: '太郎', age: 20 }

// 2. オブジェクトのマージ
const user = { name: '太郎' }
const details = { age: 20, email: 'taro@example.com' }
const merged = { ...user, ...details }
// { name: '太郎', age: 20, email: 'taro@example.com' }

// 3. プロパティの上書き(後勝ち)
const base = { name: '太郎', age: 20 }
const updated = { ...base, age: 21 }  // { name: '太郎', age: 21 }

// 4. ネストしたオブジェクトの更新(シャローコピー注意)
const user = {
  name: '太郎',
  profile: { age: 20, city: '東京' }
}
const updated = {
  ...user,
  profile: { ...user.profile, age: 21 }  // profileもコピー必要
}

実務でよく使うパターン

// パターン1: Vue/React での state 更新
const [user, setUser] = useState({ name: '太郎', age: 20 })
setUser({ ...user, age: 21 })

// パターン2: API レスポンスのマージ
const defaultSettings = { theme: 'light', notifications: true }
const userSettings = { theme: 'dark' }
const finalSettings = { ...defaultSettings, ...userSettings }
// { theme: 'dark', notifications: true }

// パターン3: 配列の immutable 更新
const todos = ref([
  { id: 1, title: 'Task 1', done: false },
  { id: 2, title: 'Task 2', done: false }
])

// 特定の要素を更新
todos.value = todos.value.map(todo =>
  todo.id === 1 ? { ...todo, done: true } : todo
)

注意点: シャローコピー

// ⚠️ シャローコピー(浅いコピー)
const original = {
  name: '太郎',
  profile: { age: 20 }
}

const copy = { ...original }
copy.profile.age = 21
// ❌ originalのprofile.ageも21に変わる!

// ✅ ネストしたオブジェクトもコピー
const copy2 = {
  ...original,
  profile: { ...original.profile }
}
copy2.profile.age = 21
// ✅ originalは変わらない

🔄 配列のループ(よく使うメソッド)

map - 各要素を変換

const numbers = [1, 2, 3]
const doubled = numbers.map(n => n * 2)  // [2, 4, 6]

// 実務例: APIレスポンスの変換
const users = [
  { id: 1, firstName: '太郎', lastName: '山田' },
  { id: 2, firstName: '花子', lastName: '田中' }
]

const fullNames = users.map(user => ({
  id: user.id,
  fullName: `${user.lastName} ${user.firstName}`
}))
// [{ id: 1, fullName: '山田 太郎' }, { id: 2, fullName: '田中 花子' }]

filter - 条件に合う要素だけ取り出す

const numbers = [1, 2, 3, 4, 5]
const evens = numbers.filter(n => n % 2 === 0)  // [2, 4]

// 実務例: 未完了のTodoだけ取得
const todos = [
  { id: 1, title: 'Task 1', done: false },
  { id: 2, title: 'Task 2', done: true },
  { id: 3, title: 'Task 3', done: false }
]

const incompleteTodos = todos.filter(todo => !todo.done)
// [{ id: 1, ... }, { id: 3, ... }]

find - 条件に合う最初の要素を取得

const users = [
  { id: 1, name: '太郎' },
  { id: 2, name: '花子' },
  { id: 3, name: '次郎' }
]

const user = users.find(u => u.id === 2)
// { id: 2, name: '花子' }

// 見つからない場合は undefined
const notFound = users.find(u => u.id === 999)  // undefined

reduce - 配列を1つの値に集約

// 基本: 合計
const numbers = [1, 2, 3, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0)  // 10

// 実務例1: オブジェクトに変換
const users = [
  { id: 1, name: '太郎' },
  { id: 2, name: '花子' }
]

const userMap = users.reduce((acc, user) => {
  acc[user.id] = user
  return acc
}, {} as Record<number, typeof users[0]>)
// { 1: { id: 1, name: '太郎' }, 2: { id: 2, name: '花子' } }

// 実務例2: グルーピング
const todos = [
  { id: 1, status: 'pending', title: 'Task 1' },
  { id: 2, status: 'done', title: 'Task 2' },
  { id: 3, status: 'pending', title: 'Task 3' }
]

const grouped = todos.reduce((acc, todo) => {
  if (!acc[todo.status]) acc[todo.status] = []
  acc[todo.status].push(todo)
  return acc
}, {} as Record<string, typeof todos>)
// { pending: [...], done: [...] }

sort - ソート(破壊的メソッド!)

const numbers = [3, 1, 4, 1, 5]

// ❌ 破壊的メソッド
numbers.sort((a, b) => a - b)  // numbersが変わる

// ✅ コピーしてからソート
const sorted = [...numbers].sort((a, b) => a - b)  // [1, 1, 3, 4, 5]

// 実務例: オブジェクトのソート
const users = [
  { name: '太郎', age: 30 },
  { name: '花子', age: 25 },
  { name: '次郎', age: 35 }
]

const sortedByAge = [...users].sort((a, b) => a.age - b.age)
// 年齢の昇順

const sortedByName = [...users].sort((a, b) =>
  a.name.localeCompare(b.name)
)
// 名前の昇順(日本語対応)

実務でのメソッドチェーン

// よくあるパターン: filter → map → sort
const users = [
  { id: 1, name: '太郎', age: 30, active: true },
  { id: 2, name: '花子', age: 25, active: false },
  { id: 3, name: '次郎', age: 35, active: true }
]

const result = users
  .filter(user => user.active)           // アクティブなユーザーだけ
  .map(user => ({ ...user, age: user.age + 1 }))  // 年齢+1
  .sort((a, b) => a.age - b.age)         // 年齢順

// パターン2: find → 更新 → 配列に戻す
const todos = [
  { id: 1, title: 'Task 1', done: false },
  { id: 2, title: 'Task 2', done: false }
]

const updateTodo = (id: number) => {
  return todos.map(todo =>
    todo.id === id ? { ...todo, done: true } : todo
  )
}

📚 丸暗記すべきパターン

1. 全プロパティを optional に

type Partial<T> = {
  [K in keyof T]?: T[K]
}

2. 全プロパティを required に

type Required<T> = {
  [K in keyof T]-?: T[K]  // -? でoptionalを外す
}

3. 全プロパティを readonly に

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

4. エラーメッセージの型

type ValidationErrors<T> = {
  [K in keyof T]?: string | undefined
}

5. ref の配列操作

// ❌ 破壊的メソッド
arr.value.push(item)

// ✅ 新しい配列を代入
arr.value = [...arr.value, item]
arr.value = arr.value.filter(...)
arr.value = [...arr.value].sort(...)

🎯 学習の優先順位

Phase 1: 絶対覚える(実務で毎日使う)

  • Partial, Pick, Omit
  • Mapped Types: { [K in keyof T]: ... }
  • keyof
  • Promise<T>
  • ref の配列操作

Phase 2: 使えると便利

  • Required, Readonly
  • Record
  • ReturnType, Parameters
  • Type Assertion

Phase 3: 高度(必要になったら)

  • Conditional Types
  • Template Literal Types
  • infer

まとめ

TypeScript の型定義は、最初は難しく感じるかもしれませんが、パターンとして覚えれば実務で自然に使えるようになります

重要なポイント:

  • ✅ Utility Types(Partial, Pick, Omit)は毎日使う
  • ✅ Mapped Types は「各プロパティを変換」するときの定石
  • ✅ keyof は型安全な動的プロパティアクセスに必須
  • ✅ スプレッド構文はシャローコピーなので、ネストに注意
  • ✅ 破壊的メソッド(push, sort)は ref で使わない

「なんとなく」使っていた TypeScript も、これらのパターンを理解すれば「ちゃんと理解して」使えるようになります。

実務で遭遇したら、この記事に戻ってパターンを確認してみてください!


参考になったら、いいね・ストックしていただけると嬉しいです! 🙌

Discussion