📘
「なんとなく」から卒業!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