TypeScript 型体操上級:入門からマスターまで

前端工程

なぜ 2026 年に高度な TypeScript をマスターすべきか

2026 年、TypeScript はフロントエンドと Node.js プロジェクトの標準となっています。しかし、ほとんどの開発者は「変数に型を付ける」段階にとどまっています。真の型プログラミング能力——ジェネリック制約、条件型、infer 推論、マップ型——こそが、ジュニアとシニア TypeScript 開発者を分ける分水嶺です。型体操をマスターすれば、コンパイル時に多くのエラーを捕捉し、ランタイムバグを減らし、コードの自己文書化能力を大幅に向上させることができます。

型体操が解決できる問題

  • コンパイル時検証:ランタイムエラーをコンパイル時に前倒し — API レスポンスのフィールド欠落、フォームバリデーションルールの不一致など
  • コード補完の強化:正確なインテリセンスで開発効率が倍増、ドキュメント検索の頻度を削減
  • リファクタリングのセーフティネット:型システムはリファクタリングの最強の保障 — フィールドの型を変更すれば、すべての参照箇所が即座にエラーに
  • ドメインモデリング:branded types、discriminated unions でビジネスルールを型レベルで表現
  • ゼロランタイムオーバーヘッド:すべての型体操はコンパイル後に完全に消去され、バンドルサイズやパフォーマンスに影響なし

単純な型から型プログラミングへの進化

// 初級:変数に型を付ける
const username: string = '田中'
const age: number = 25

// 中級:ジェネリックとインターフェースの使用
interface ApiResponse<T> {
  code: number
  data: T
  message: string
}

// 上級:型プログラミング — 型レベルで計算する
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}

type User = {
  name: string
  profile: { avatar: string; bio: string }
}
type FrozenUser = DeepReadonly<User>
// { readonly name: string; readonly profile: { readonly avatar: string; readonly bio: string } }

💡 JSON to TypeScript ツール を使用して、API レスポンスから型定義を素早く生成し、その上に高度な型プログラミングを構築できます。


ジェネリック制約とデフォルト値

extends でジェネリックの範囲を制約

ジェネリック制約により、型パラメータの範囲を制限し、不正な型の渡しを防ぎます。

// T に id プロパティが必須という制約
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}

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

const users: User[] = [{ id: 1, name: '田中' }]
const products: Product[] = [{ id: 1, title: 'TypeScript Book', price: 99 }]

findById(users, 1)      // ✅ User | undefined
findById(products, 1)   // ✅ Product | undefined
findById([1, 2, 3], 1)  // ❌ number には id プロパティがない

keyof でプロパティ名を制約

// K を T のキーに制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: '田中', age: 25, email: 'tanaka@test.com' }
getProperty(user, 'name')   // ✅ string
getProperty(user, 'age')    // ✅ number
getProperty(user, 'phone')  // ❌ 'phone' は user のプロパティではない

ジェネリックのデフォルト値

interface PaginatedResponse<T, Meta = { total: number; page: number }> {
  data: T[]
  meta: Meta
}

type UserList = PaginatedResponse<User>
// meta のデフォルトは { total: number; page: number }

type CustomMeta = { total: number; page: number; hasNext: boolean }
type UserListCustom = PaginatedResponse<User, CustomMeta>
// meta にカスタム型を使用

複数ジェネリック制約の組み合わせ

// T は両方の制約を満たす必要がある
type RequireBoth<T extends A & B, A, B> = T

interface HasId { id: number }
interface HasName { name: string }

function mergeEntities<T extends HasId & HasName>(a: T, b: T): T[] {
  return [a, b]
}

mergeEntities({ id: 1, name: '田中' }, { id: 2, name: '佐藤' }) // ✅
mergeEntities({ id: 1 }, { id: 2 }) // ❌ name が欠落

条件型:extends ? :

基本構文

条件型は型レベルの三項演算子のようなものです:T extends U ? X : Y

type IsString<T> = T extends string ? 'yes' : 'no'

type A = IsString<string>   // 'yes'
type B = IsString<number>   // 'no'
type C = IsString<'hello'>  // 'yes'(リテラル型も string にマッチ)

分配条件型

T がユニオン型の場合、条件型は自動的に分配されます:

type ToArray<T> = T extends unknown ? T[] : never

type Result = ToArray<string | number>
// 分配プロセス:ToArray<string> | ToArray<number>
// 結果:string[] | number[]

// 注意:これは (string | number)[] ではありません

分配の防止

[T] でラップして分配を防ぎます:

type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never

type Result2 = ToArrayNoDistribute<string | number>
// 結果:(string | number)[]、分配なし

実践:型フィルタリング

// ユニオン型から特定の型を除外
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never

type AllTypes = string | number | boolean | null | undefined

type NonNull = Exclude<AllTypes, null | undefined>
// string | number | boolean

type OnlyString = Extract<AllTypes, string>
// string

// オブジェクト型のプロパティを値の型でフィルタリング
type PickByValue<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K]
}

interface FormState {
  username: string
  age: number
  email: string
  isActive: boolean
}

type StringFields = PickByValue<FormState, string>
// { username: string; email: string }

infer キーワード:深掘り

infer の基本:条件型で型を抽出

inferextends 条件型内でのみ使用でき、推論される型変数を宣言します。

// 関数の戻り値の型を抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

function getUser() {
  return { name: '田中', age: 25 }
}

type UserType = ReturnType<typeof getUser>
// { name: string; age: number }

// 関数のパラメータ型を抽出
type Parameters<T> = T extends (...args: infer P) => any ? P : never

function greet(name: string, age: number): string {
  return `Hello, ${name}, age ${age}`
}

type GreetParams = Parameters<typeof greet>
// [string, number]

Promise の内部型を抽出

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

type P1 = Awaited<Promise<string>>          // string
type P2 = Awaited<Promise<Promise<number>>> // number(再帰的アンラップ)
type P3 = Awaited<string>                   // string(非 Promise は直接返す)

配列要素型の抽出

type ElementOf<T> = T extends (infer E)[] ? E : never

type Items = ElementOf<string[]>       // string
type Nums = ElementOf<number[]>        // number
type Mixed = ElementOf<(string | number)[]> // string | number

// タプルの最初の要素を抽出
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never

type First = Head<[string, number, boolean]> // string

// タプルの最後の要素を抽出
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never

type Tail = Last<[string, number, boolean]> // boolean

複数 infer の協調推論

// クラスコンストラクタからインスタンス型を抽出
type InstanceOf<T> = T extends new (...args: any[]) => infer I ? I : never

class UserService {
  constructor(private id: number) {}
  getUser() { return { id: this.id } }
}

type ServiceInstance = InstanceOf<typeof UserService>
// UserService

// リクエストとレスポンスの型を同時に抽出
type ApiTypes<T> = T extends (req: infer Req) => Promise<infer Res>
  ? { request: Req; response: Res }
  : never

async function updateUser(req: { id: number; name: string }): Promise<{ success: boolean }> {
  return { success: true }
}

type UpdateUserTypes = ApiTypes<typeof updateUser>
// { request: { id: number; name: string }; response: { success: boolean } }

マップ型:組み込みユーティリティ型をゼロから実装

Record の実装

// 組み込み Record<K, V> の実装原理
type MyRecord<K extends keyof any, V> = {
  [P in K]: V
}

type UserRoles = MyRecord<'admin' | 'editor' | 'viewer', boolean>
// { admin: boolean; editor: boolean; viewer: boolean }

// 実践:列挙マッピングの作成
type StatusMap = MyRecord<'pending' | 'active' | 'closed', { label: string; color: string }>

const statusConfig: StatusMap = {
  pending: { label: '保留中', color: '#f59e0b' },
  active: { label: '進行中', color: '#10b981' },
  closed: { label: '終了', color: '#6b7280' },
}

Partial と Required の実装

// Partial:すべてのプロパティをオプショナルに
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

// Required:すべてのプロパティを必須に
type MyRequired<T> = {
  [K in keyof T]-?: T[K]
}

// 深い Partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K]
}

interface Config {
  db: { host: string; port: number }
  cache: { ttl: number; max: number }
}

type PartialConfig = DeepPartial<Config>
// { db?: { host?: string; port?: number }; cache?: { ttl?: number; max?: number } }

Pick と Omit の実装

// Pick:プロパティのサブセットを選択
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// Omit:プロパティのサブセットを除外
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

interface FullUser {
  id: number
  name: string
  email: string
  password: string
  createdAt: string
}

// 安全なユーザー情報、パスワードを除外
type SafeUser = MyOmit<FullUser, 'password'>
// { id: number; name: string; email: string; createdAt: string }

// 一部のフィールドのみ更新
type UserUpdate = MyPartial<MyPick<FullUser, 'name' | 'email'>>
// { name?: string; email?: string }

キーリマッピング

TypeScript 4.1+ は as によるキーリマッピングをサポート:

// プロパティ名にプレフィックスを追加
type AddPrefix<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K]
}

interface ApiData { name: string; age: number }
type PrefixedData = AddPrefix<ApiData, 'user'>
// { userName: string; userAge: number }

// すべてのプロパティをゲッターに変換
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }

テンプレートリテラル型

基本的な使用法

type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'

// CSS プロパティ型
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'
type CSSValue = `${number}${CSSUnit}`

const width: CSSValue = '100px'   // ✅
const height: CSSValue = '50vh'   // ✅
const invalid: CSSValue = 'abc'   // ❌ テンプレートにマッチしない

マップ型と組み合わせた型安全なイベントシステム

type Events = {
  click: { x: number; y: number }
  keydown: { key: string; code: string }
  resize: { width: number; height: number }
}

type EventHandlerMap = {
  [K in keyof Events as `on${Capitalize<string & K>}`]: (payload: Events[K]) => void
}

type Listeners = EventHandlerMap
// {
//   onClick: (payload: { x: number; y: number }) => void
//   onKeyDown: (payload: { key: string; code: string }) => void
//   onResize: (payload: { width: number; height: number }) => void
// }

const listeners: Listeners = {
  onClick: (e) => console.log(e.x, e.y),
  onKeyDown: (e) => console.log(e.key),
  onResize: (e) => console.log(e.width),
}

文字列解析型

// ルートパラメータの解析
type ParseRouteParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ParseRouteParams<Rest>
    : S extends `${string}:${infer Param}`
      ? Param
      : never

type Params = ParseRouteParams<'/user/:id/post/:postId'>
// 'id' | 'postId'

// バージョン番号の解析
type ParseVersion<V extends string> =
  V extends `${infer Major}.${infer Minor}.${infer Patch}`
    ? { major: Major; minor: Minor; patch: Patch }
    : never

type V = ParseVersion<'1.2.3'>
// { major: '1'; minor: '2'; patch: '3' }

再帰型

深い Readonly

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

const config: DeepReadonly<{
  db: { host: string; port: number }
  cache: { ttl: number }
}> = {
  db: { host: 'localhost', port: 3306 },
  cache: { ttl: 3600 },
}

// config.db.host = '127.0.0.1'  // ❌ 読み取り専用

深い Partial とマージ

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

type DeepMerge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B
    ? B[K] extends object
      ? K extends keyof A
        ? A[K] extends object
          ? DeepMerge<A[K], B[K]>
          : B[K]
        : B[K]
      : B[K]
    : K extends keyof A
      ? A[K]
      : never
}

再帰的タプル操作

// タプルの反転
type Reverse<T extends any[]> = T extends [infer H, ...infer R]
  ? [...Reverse<R>, H]
  : []

type Reversed = Reverse<[1, 2, 3, 4]> // [4, 3, 2, 1]

// タプルのフラット化
type Flatten<T extends any[]> = T extends [infer H, ...infer R]
  ? H extends any[]
    ? [...Flatten<H>, ...Flatten<R>]
    : [H, ...Flatten<R>]
  : []

type Flat = Flatten<[1, [2, 3], [4, [5, 6]]]> // [1, 2, 3, 4, 5, 6]

// 文字列の長さ
type StrLen<S extends string, Acc extends any[] = []> =
  S extends `${string}${infer Rest}`
    ? StrLen<Rest, [...Acc, 0]>
    : Acc['length']

type Len = StrLen<'hello'> // 5

型レベルプログラミング:算術と文字列操作

型レベルの加算

// タプル長を使用して加算を実装
type BuildTuple<N extends number, T extends any[] = []> =
  T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>

type Add<A extends number, B extends number> =
  [...BuildTuple<A>, ...BuildTuple<B>]['length']

type Sum = Add<3, 5> // 8

// 型レベルの減算
type Subtract<A extends number, B extends number> =
  BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
    ? Rest['length']
    : never

type Diff = Subtract<10, 4> // 6

型レベルの文字列操作

// 文字列の置換
type Replace<S extends string, From extends string, To extends string> =
  From extends ''
    ? S
    : S extends `${infer Before}${From}${infer After}`
      ? `${Before}${To}${After}`
      : S

type Replaced = Replace<'hello world', 'world', 'TypeScript'>
// 'hello TypeScript'

// キャメルケースからスネークケースへ
type CamelToSnake<S extends string> =
  S extends `${infer H}${infer Rest}`
    ? H extends Uppercase<H>
      ? `_${Lowercase<H>}${CamelToSnake<Rest>}`
      : `${H}${CamelToSnake<Rest>}`
    : S

type Snake = CamelToSnake<'userName'> // 'user_name'

判別可能なユニオン型

基本パターン

// kind フィールドを判別値として使用
interface Circle { kind: 'circle'; radius: number }
interface Rectangle { kind: 'rectangle'; width: number; height: number }
interface Triangle { kind: 'triangle'; base: number; height: number }

type Shape = Circle | Rectangle | Triangle

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2
    case 'rectangle': return shape.width * shape.height
    case 'triangle': return 0.5 * shape.base * shape.height
  }
}

ステートマシンパターン

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function handleState<T>(state: RequestState<T>) {
  switch (state.status) {
    case 'idle': return 'リクエスト待ち'
    case 'loading': return '読み込み中...'
    case 'success': return `データ: ${JSON.stringify(state.data)}`
    case 'error': return `エラー: ${state.error.message}`
  }
}

// 状態遷移は型安全
const idleState: RequestState<string> = { status: 'idle' }
// idleState.data // ❌ idle 状態には data がない
const successState: RequestState<string> = { status: 'success', data: 'hello' }
// successState.data // ✅ string

網羅チェック

// never 型を使用してすべての分岐が処理されていることを確認
function assertNever(x: never): never {
  throw new Error(`未処理の型: ${JSON.stringify(x)}`)
}

function processShape(shape: Shape) {
  switch (shape.kind) {
    case 'circle': return /* ... */ ''
    case 'rectangle': return /* ... */ ''
    case 'triangle': return /* ... */ ''
    default: return assertNever(shape) // case の漏れがあるとここでエラー
  }
}

Branded Types:型安全性の武器

基本的な Branded Type

// ブランド型で同じ構造だが異なる意味を持つ型を区別
type Brand<T, B extends string> = T & { __brand: B }

type UserId = Brand<number, 'UserId'>
type OrderId = Brand<number, 'OrderId'>

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const uid = 1 as UserId
const oid = 2 as OrderId

getUser(uid) // ✅
getUser(oid) // ❌ OrderId を UserId に割り当てられない
getOrder(1)  // ❌ number を UserId に割り当てられない

型安全な ID システム

function createId<T extends string>(brand: T) {
  return (id: number) => id as Brand<number, T>
}

const userId = createId('UserId')
const orderId = createId('OrderId')

function deleteUser(id: UserId) { /* ... */ }

deleteUser(userId(1))  // ✅
deleteUser(orderId(1)) // ❌ 型安全性がエラーを防止

型安全な通貨システム

type USD = Brand<number, 'USD'>
type CNY = Brand<number, 'CNY'>

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD
}

function addCNY(a: CNY, b: CNY): CNY {
  return (a + b) as CNY
}

const price1 = 100 as USD
const price2 = 50 as USD
const cnyPrice = 200 as CNY

addUSD(price1, price2)    // ✅
addUSD(price1, cnyPrice)  // ❌ CNY と USD を混ぜることはできない

satisfies 演算子

基本的な使用法

satisfies を使用すると、型を拡大せずに型の互換性を検証できます:

// ❌ 型注釈は型を拡大する
const colors: Record<string, string | string[]> = {
  primary: '#3b82f6',
  secondary: '#6b7280',
  gradients: ['#3b82f6', '#6b7280'],
}
// colors.primary.toUpperCase() // ❌ 型は string | string[]

// ✅ satisfies でリテラル型を保持
const themeColors = {
  primary: '#3b82f6',
  secondary: '#6b7280',
  gradients: ['#3b82f6', '#6b7280'],
} satisfies Record<string, string | string[]>

themeColors.primary.toUpperCase()  // ✅ 型は '#3b82f6'
themeColors.gradients[0].toUpperCase() // ✅ 型は string

設定オブジェクトの検証

interface FeatureConfig {
  enabled: boolean
  label: string
}

const features = {
  darkMode: { enabled: true, label: 'ダークモード' },
  notifications: { enabled: false, label: '通知' },
  analytics: { enabled: true, label: '分析' },
} satisfies Record<string, FeatureConfig>

// features の型はすべてのキー名を保持
features.darkMode.enabled   // ✅ boolean
features.notifications.label // ✅ string
// features.nonExist.enabled  // ❌ プロパティが存在しない

const 型パラメータ

ジェネリック推論でのリテラル型の保持

TypeScript 5.0+ の const 型パラメータにより、ジェネリック推論でリテラル型を保持できます:

// ❌ const なしでは T は string と推論される
function createRoute<T>(path: T) {
  return { path }
}
const route = createRoute('/user/:id')
// route.path の型は string、'/user/:id' ではない

// ✅ const 型パラメータを使用
function createRouteConst<const T>(path: T) {
  return { path }
}
const routeConst = createRouteConst('/user/:id')
// routeConst.path の型は '/user/:id'

テンプレートリテラル型との組み合わせ

function defineEndpoints<const T extends Record<string, string>>(routes: T) {
  type RouteKeys = keyof T
  return routes as T & { _routeKeys: RouteKeys }
}

const api = defineEndpoints({
  getUsers: '/api/users',
  getUser: '/api/users/:id',
  createUser: '/api/users',
})

// api のキー名はリテラル型 'getUsers' | 'getUser' | 'createUser' として保持

実践パターン

型安全な API レスポンス

interface ApiRoutes {
  '/api/users': {
    response: { users: { id: number; name: string }[] }
  }
  '/api/users/:id': {
    params: { id: number }
    response: { id: number; name: string; email: string }
  }
  '/api/orders': {
    query: { status?: string; page?: number }
    response: { orders: { id: number; total: number }[]; total: number }
  }
}

type ApiRoute = keyof ApiRoutes

async function apiCall<
  Route extends ApiRoute,
  Config extends ApiRoutes[Route]
>(
  route: Route,
  options: Omit<Config, 'response'> extends object ? Config : {}
): Promise<Config extends { response: infer R } ? R : never> {
  const res = await fetch(route as string)
  return res.json()
}

// 型安全な API 呼び出し
const users = await apiCall('/api/users', {})
// users の型: { users: { id: number; name: string }[] }

フォームバリデーション型

type Validator<T> = (value: T) => string | null

type FormValidators<T> = {
  [K in keyof T]?: Validator<T[K]> | Validator<T[K]>[]
}

interface LoginForm {
  username: string
  password: string
  remember: boolean
}

const loginValidators: FormValidators<LoginForm> = {
  username: (v) => v.length < 3 ? 'ユーザー名は3文字以上必要' : null,
  password: (v) => v.length < 6 ? 'パスワードは6文字以上必要' : null,
}

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

function validateForm<T>(
  data: T,
  validators: FormValidators<T>
): ValidationError<T> {
  const errors: ValidationError<T> = {}
  for (const key in validators) {
    const validator = validators[key]
    const value = data[key]
    if (validator && value !== undefined) {
      const result = Array.isArray(validator)
        ? validator.map(v => v(value)).filter(Boolean)
        : validator(value)
      if (result) errors[key] = Array.isArray(result) ? result : [result]
    }
  }
  return errors
}

イベントシステム型

interface EventMap {
  'user:login': { userId: number; timestamp: number }
  'user:logout': { userId: number }
  'cart:add': { productId: number; quantity: number }
  'cart:remove': { productId: number }
  'order:create': { orderId: number; total: number }
}

type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void

class TypeSafeEventEmitter {
  private handlers = new Map<string, Set<EventHandler<any>>>()

  on<K extends keyof EventMap>(event: K, handler: EventHandler<K>) {
    if (!this.handlers.has(event as string)) {
      this.handlers.set(event as string, new Set())
    }
    this.handlers.get(event as string)!.add(handler)
    return () => this.off(event, handler)
  }

  off<K extends keyof EventMap>(event: K, handler: EventHandler<K>) {
    this.handlers.get(event as string)?.delete(handler)
  }

  emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
    this.handlers.get(event as string)?.forEach(h => h(payload))
  }
}

const emitter = new TypeSafeEventEmitter()
emitter.on('user:login', (e) => console.log(e.userId))  // ✅
emitter.emit('cart:add', { productId: 1, quantity: 2 })  // ✅
// emitter.emit('cart:add', { wrong: true })  // ❌ 型エラー

よくある型エラーと修正

エラー1:型が意図せず拡大される

// ❌ オブジェクトリテラルの型が拡大される
const config = { port: 3000, host: 'localhost' }
// config.port の型は number、3000 ではない

// ✅ as const を使用
const configConst = { port: 3000, host: 'localhost' } as const
// configConst.port の型は 3000

エラー2:ジェネリックがユニオン型として推論される

// ❌ T は string | number として推論され、分配されない
function wrapInArray<T>(value: T): T[] {
  return [value]
}
const result = wrapInArray(Math.random() > 0.5 ? 'hello' : 42)
// result の型は (string | number)[]、string[] | number[] ではない

// ✅ 関数オーバーロードまたは条件型を使用
function wrapInArrayFixed<T>(value: T): [T] {
  return [value]
}

エラー3:循環参照

// ❌ 型の循環参照
interface TreeNode {
  value: string
  children: TreeNode[]  // ✅ インターフェースは循環参照をサポート
}

// ❌ type エイリアスは循環参照をサポートしない場合がある
// type Node = { value: string; children: Node[] } // エラーになる可能性

// ✅ インターフェースを使用するか、ジェネリックで間接参照
type Node<T = Node> = { value: string; children: T[] }

エラー4:any の汚染

// ❌ any は触れるすべての型を汚染する
function parseJSON(str: string): any {
  return JSON.parse(str)
}
const data = parseJSON('{"name":"田中"}')
data.nonExist.method() // エラーにならないが、ランタイムでクラッシュ

// ✅ unknown を使用
function parseJSONSafe(str: string): unknown {
  return JSON.parse(str)
}
const safeData = parseJSONSafe('{"name":"田中"}')
// safeData.name // ❌ unknown には name プロパティが存在しない
if (typeof safeData === 'object' && safeData !== null && 'name' in safeData) {
  console.log((safeData as { name: string }).name) // ✅
}

型デバッグテクニック

中間型の検査

// 型エラーを利用して型を検査
type Debug<T> = { [K in keyof T]: T[K] }

type Result = Debug<SomeComplexType>
// Result にホバーすると展開された型が表示される

// ユーティリティ型でデバッグを補助
type Prettify<T> = { [K in keyof T]: T[K] } & {}

type Nested = { a: { b: { c: string } }; d: number }
type PrettyNested = Prettify<Nested>
// { a: { b: { c: string } }; d: number } に展開

条件型の段階的デバッグ

// 複雑な条件型を段階的に分解
type Step1<T> = T extends string ? true : false
type Step2<T> = T extends `${infer H}${infer R}` ? { head: H; rest: R } : never

// レイヤーごとに検証
type S1 = Step1<'hello'>   // true
type S2 = Step2<'hello'>   // { head: 'h'; rest: 'ello' }

よくある質問 FAQ

Q1: 型体操はランタイムパフォーマンスに影響しますか?

いいえ。TypeScript のすべての型情報はコンパイル後に完全に消去されます。型体操はコンパイル時のみに存在し、ランタイムパフォーマンスへの影響はゼロです。

Q2: 型体操とランタイムバリデーションはいつ使い分けるべきですか?

両方を組み合わせて使用:型体操はコンパイル時検証(開発中のエラー検出)を担当し、ランタイムバリデーション(Zod、Joi など)は外部データ(API レスポンス、ユーザー入力)の検証を担当します。型体操はランタイムバリデーションを代替できません。

Q3: 再帰型の深さに制限はありますか?

はい。TypeScript には再帰型の深さ制限(約1000レベル)があります。超えると「Type instantiation is excessively deep」エラーが発生します。実践では、10レベル以内の再帰で通常十分です。

Q4: チームで型体操を普及させるには?

  1. 単純なユーティリティ型(Partial、Pick、Omit)から始める
  2. チームの型ユーティリティライブラリ(types/utils.ts)を構築
  3. コードレビューで重要な関数に完全な型を要求
  4. JSON フォーマッター を使用して JSON データ構造を検証し、型定義との整合性を確保

Q5: satisfies と型注釈の違いは?

型注釈(:)は型を注釈型に拡大しますが、satisfies は検証のみを行い推論型を変更しません。リテラル型の精度を保持したい場合は satisfies を、明確な型制約が必要な場合は型注釈を使用してください。

💡 Base64 エンコードツール を使用して、型定義内のインラインデータを処理し、外部依存を削減できます。

ブラウザローカルツールを無料で試す →

#TypeScript#类型体操#泛型#条件类型#教程