Skip to content
Published on

TypeScript上級パターン完全攻略:ジェネリクスから型体操まで実践ガイド2025

Authors

はじめに

TypeScriptはJavaScriptエコシステムの標準(ひょうじゅん)となりました。2024-2025年にかけてTypeScript 5.4から5.7まで急速(きゅうそく)に進化(しんか)し、型(かた)システムの表現力(ひょうげんりょく)はさらに強力(きょうりょく)になりました。単(たん)に型(かた)を付(つ)けるレベルを超(こ)え、型レベルでロジックを実装(じっそう)し、ランタイム安全性(あんぜんせい)まで保証(ほしょう)することが現代(げんだい)TypeScript開発(かいはつ)の核心(かくしん)です。

このガイドでは、ジェネリクスの基礎(きそ)からConditional Types、Mapped Types、Template Literal Types、ブランデッド型(かた)、Zod + tRPC統合(とうごう)、そして型体操(かたたいそう)(Type Challenges)まで、TypeScript上級(じょうきゅう)パターンを体系的(たいけいてき)に整理(せいり)します。各(かく)セクションには実践(じっせん)ですぐに適用(てきよう)できるコード例(れい)が含(ふく)まれています。


1. TypeScript 5.x 新機能(しんきのう)(2024-2025)

TypeScript 5.4 — NoInferとgroupBy

TypeScript 5.4で最(もっと)も注目(ちゅうもく)すべき機能(きのう)はNoInferユーティリティ型(かた)です。ジェネリック関数(かんすう)で特定(とくてい)のパラメータが型推論(かたすいろん)に影響(えいきょう)を与(あた)えないように制御(せいぎょ)できます。

// NoInferなし — defaultValueがTの推論に影響する
function getOrDefault<T>(value: T | undefined, defaultValue: T): T {
  return value ?? defaultValue
}

// NoInfer使用 — defaultValueはTの推論に参加しない
function getOrDefault<T>(value: T | undefined, defaultValue: NoInfer<T>): T {
  return value ?? defaultValue
}

// 使用例
const result = getOrDefault('hello', 42)
// NoInferなし: T = string | number
// NoInferあり: T = string, 42でエラー発生

Object.groupByMap.groupByも正式(せいしき)にサポートされます。

const users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob', role: 'user' },
  { name: 'Charlie', role: 'admin' },
]

const grouped = Object.groupBy(users, (user) => user.role)
// 型: Partial<Record<string, User[]>>

TypeScript 5.5 — 推論された型述語(すいろんされたかたじゅつご)

TypeScript 5.5では、型(かた)ガードを明示的(めいじてき)に宣言(せんげん)しなくてもコンパイラが自動的(じどうてき)に型絞(かたしぼ)り込(こ)みを推論(すいろん)します。

// 以前: 明示的な型ガードが必要
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

// TS 5.5: 自動推論
const values = [1, 'hello', null, 'world', 3]
const strings = values.filter((v) => typeof v === 'string')
// TS 5.5ではstringsの型はstring[](以前は(string | number | null)[])

isolatedDeclarationsオプションは.d.ts生成(せいせい)を外部(がいぶ)ツール(SWC、oxcなど)に委任(いにん)できるようにします。

{
  "compilerOptions": {
    "isolatedDeclarations": true,
    "declaration": true
  }
}

TypeScript 5.6 — Iteratorヘルパー

ECMAScript Iterator Helpers提案(ていあん)をサポートし、イテレータをチェーン方式(ほうしき)で操作(そうさ)できます。

function* fibonacci(): Generator<number> {
  let a = 0,
    b = 1
  while (true) {
    yield a
    ;[a, b] = [b, a + b]
  }
}

// Iterator helpers (TS 5.6)
const first10Even = fibonacci()
  .filter((n) => n % 2 === 0)
  .take(10)
  .toArray()

--noUncheckedSideEffectImportsフラグはサイドエフェクトインポートの存在(そんざい)を検証(けんしょう)します。

// このファイルが存在しなければエラー発生
import './polyfills.js'
import 'some-module/register'

TypeScript 5.7 — ES2024ターゲットとパス書(か)き換(か)え

{
  "compilerOptions": {
    "target": "es2024",
    "rewriteRelativeImportExtensions": true
  }
}

rewriteRelativeImportExtensionsオプションは.tsインポートを出力(しゅつりょく)時(じ)に.jsに自動(じどう)変換(へんかん)します。

// ソースコード
import { helper } from './utils.ts'

// コンパイル後の出力
import { helper } from './utils.js'

2. ジェネリクス マスタークラス

基本(きほん)ジェネリクス

ジェネリクスは型(かた)をパラメータとして受(う)け取(と)り、再利用(さいりよう)可能(かのう)なコードを作成(さくせい)するTypeScriptの核心(かくしん)機能(きのう)です。

// 最も基本的なジェネリック関数
function identity<T>(arg: T): T {
  return arg
}

// ジェネリックインターフェース
interface Box<T> {
  value: T
  map<U>(fn: (value: T) => U): Box<U>
}

function createBox<T>(value: T): Box<T> {
  return {
    value,
    map: (fn) => createBox(fn(value)),
  }
}

const numberBox = createBox(42) // Box<number>
const stringBox = numberBox.map(String) // Box<string>

制約条件(せいやくじょうけん)(extends)

// 基本制約条件
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length
}

getLength('hello') // OK
getLength([1, 2, 3]) // OK
getLength(42) // エラー: numberにはlengthがない

// keyof制約条件
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Alice', age: 30 }
getProperty(user, 'name') // string
getProperty(user, 'age') // number
getProperty(user, 'foo') // エラー: 'foo'はkeyof Userではない

ジェネリックデフォルト値(ち)

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

// Metaデフォルト値を使用
const response: PaginatedResponse<User> = {
  data: [{ name: 'Alice', age: 30 }],
  meta: { page: 1, total: 100 },
}

// Metaをカスタマイズ
const cursorResponse: PaginatedResponse<User, { cursor: string; hasMore: boolean }> = {
  data: [{ name: 'Bob', age: 25 }],
  meta: { cursor: 'abc123', hasMore: true },
}

実践例(じっせんれい): APIレスポンスラッパー

// APIレスポンス型体系
interface ApiResponse<T> {
  success: true
  data: T
  timestamp: number
}

interface ApiError {
  success: false
  error: {
    code: string
    message: string
  }
  timestamp: number
}

type ApiResult<T> = ApiResponse<T> | ApiError

// 型安全なAPIクライアント
async function fetchApi<T>(url: string): Promise<ApiResult<T>> {
  const response = await fetch(url)
  return response.json()
}

// 使用
interface User {
  id: number
  name: string
  email: string
}

const result = await fetchApi<User>('/api/users/1')

if (result.success) {
  console.log(result.data.name) // 型安全: User
} else {
  console.error(result.error.message) // 型安全: ApiError
}

実践例: Repositoryパターン

interface Entity {
  id: string
  createdAt: Date
  updatedAt: Date
}

interface Repository<T extends Entity> {
  findById(id: string): Promise<T | null>
  findMany(filter: Partial<T>): Promise<T[]>
  create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>
  update(id: string, data: Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>): Promise<T>
  delete(id: string): Promise<void>
}

// 実装
interface User extends Entity {
  name: string
  email: string
  role: 'admin' | 'user'
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    return null
  }

  async findMany(filter: Partial<User>): Promise<User[]> {
    return []
  }

  async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
    return {} as User
  }

  async update(
    id: string,
    data: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
  ): Promise<User> {
    return {} as User
  }

  async delete(id: string): Promise<void> {}
}

実践例: Builderパターン

class QueryBuilder<T extends Record<string, unknown>> {
  private conditions: string[] = []
  private selectedFields: string[] = []

  select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
    this.selectedFields = fields as string[]
    return this as unknown as QueryBuilder<Pick<T, K>>
  }

  where<K extends keyof T>(field: K, value: T[K]): this {
    this.conditions.push(`${String(field)} = ${JSON.stringify(value)}`)
    return this
  }

  build(): { fields: string[]; conditions: string[] } {
    return {
      fields: this.selectedFields,
      conditions: this.conditions,
    }
  }
}

// 使用
interface Product {
  id: number
  name: string
  price: number
  category: string
}

const query = new QueryBuilder<Product>()
  .select('name', 'price')
  .where('category', 'electronics')
  .build()

ジェネリック推論(すいろん)(infer)深掘(ふかぼ)り

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

// Promiseの内部型を抽出
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

// 配列の要素型を抽出
type ElementOf<T> = T extends (infer E)[] ? E : never

// 実践: 複数のinferを一緒に使用
type FunctionParts<T> = T extends (...args: infer A) => infer R ? { args: A; return: R } : never

type Parts = FunctionParts<(name: string, age: number) => boolean>
// { args: [string, number]; return: boolean }

3. Conditional Types 深掘り

inferキーワードで型(かた)を抽出(ちゅうしゅつ)

Conditional Typesのinferキーワードは型(かた)を「キャプチャ」する役割(やくわり)を果(は)たします。複雑(ふくざつ)な型(かた)から必要(ひつよう)な部分(ぶぶん)を抽出(ちゅうしゅつ)する際(さい)に不可欠(ふかけつ)です。

// 関数の最初のパラメータ型を抽出
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never

type P = FirstParam<(name: string, age: number) => void> // string

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

class MyClass {
  value = 42
}

type Instance = InstanceOf<typeof MyClass> // MyClass

再帰的(さいきてき)Conditional Types

// ディープReadonly
type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T

interface Config {
  db: {
    host: string
    port: number
    credentials: {
      user: string
      password: string
    }
  }
  features: string[]
}

type ReadonlyConfig = DeepReadonly<Config>
// すべてのネストされたプロパティがreadonlyになる

// ディープPartial
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T

// JSON型(再帰)
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }

分配的(ぶんぱいてき)Conditional Types

ユニオン型(かた)にConditional Typesを適用(てきよう)すると、各(かく)メンバーに対(たい)して個別(こべつ)に適用(てきよう)されます。

// 分配が適用される場合
type ToArray<T> = T extends any ? T[] : never

type Result = ToArray<string | number>
// string[] | number[](分配される)

// 分配を防止するには角括弧で囲む
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never

type Result2 = ToArrayNonDist<string | number>
// (string | number)[](分配されない)

実践: APIレスポンスからデータ型(かた)を抽出(ちゅうしゅつ)

// APIエンドポイント型定義
interface ApiEndpoints {
  '/users': { response: User[]; params: never }
  '/users/:id': { response: User; params: { id: string } }
  '/posts': { response: Post[]; params: never }
  '/posts/:id': { response: Post; params: { id: string } }
}

// レスポンス型を抽出
type ResponseOf<T extends keyof ApiEndpoints> = ApiEndpoints[T]['response']
type ParamsOf<T extends keyof ApiEndpoints> = ApiEndpoints[T]['params']

// 使用
type UserListResponse = ResponseOf<'/users'> // User[]
type UserParams = ParamsOf<'/users/:id'> // { id: string }

Promise Unwrap(深(ふか)いラップ解除(かいじょ))

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

type A = DeepAwaited<Promise<Promise<Promise<string>>>> // string

// 実践: 非同期関数の実際の戻り値型
type AsyncReturnType<T extends (...args: any[]) => Promise<any>> = T extends (
  ...args: any[]
) => Promise<infer R>
  ? R
  : never

async function fetchUser(): Promise<User> {
  return {} as User
}

type FetchedUser = AsyncReturnType<typeof fetchUser> // User

4. Mapped TypesとTemplate Literal Types

組み込みユーティリティ型(かた)の直接(ちょくせつ)実装(じっそう)

TypeScriptの組(く)み込(こ)みユーティリティ型(かた)がどのように動作(どうさ)するかを理解(りかい)すれば、カスタムユーティリティを作成(さくせい)できます。

// Partial実装
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

// Required実装
type MyRequired<T> = {
  [K in keyof T]-?: T[K]
}

// Readonly実装
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 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]
}

Keyリマッピング(as)

TypeScript 4.1以降(いこう)、as節(せつ)を使用(しよう)するとキーを変換(へんかん)できます。

// すべてのキーにプレフィックスを追加
type Prefixed<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K]
}

interface User {
  name: string
  age: number
}

type PrefixedUser = Prefixed<User, 'get'>
// { getName: string; getAge: number }

// Getter型を自動生成
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

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

// 特定の型のキーだけをフィルタリング
type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}

interface Mixed {
  name: string
  age: number
  email: string
  active: boolean
}

type OnlyStrings = StringKeys<Mixed>
// { name: string; email: string }

Template Literal Types

文字列(もじれつ)リテラル型(かた)を組(く)み合(あ)わせて強力(きょうりょく)な型(かた)を生成(せいせい)できます。

// 基本テンプレートリテラル
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' // OK
const height: CSSValue = '50vh' // OK

// 組み込み文字列操作型
type Upper = Uppercase<'hello'> // 'HELLO'
type Lower = Lowercase<'HELLO'> // 'hello'
type Cap = Capitalize<'hello'> // 'Hello'
type Uncap = Uncapitalize<'Hello'> // 'hello'

実践: イベントハンドラー型(かた)の自動生成(じどうせいせい)

interface Events {
  userCreated: { userId: string; name: string }
  userUpdated: { userId: string; changes: Record<string, unknown> }
  orderPlaced: { orderId: string; total: number }
  orderCancelled: { orderId: string; reason: string }
}

// イベントリスナー型を自動生成
type EventListeners<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void
}

type AppEventListeners = EventListeners<Events>
// {
//   onUserCreated: (payload: { userId: string; name: string }) => void;
//   onUserUpdated: (payload: { userId: string; changes: ... }) => void;
//   onOrderPlaced: (payload: { orderId: string; total: number }) => void;
//   onOrderCancelled: (payload: { orderId: string; reason: string }) => void;
// }

実践: APIルート型(かた)の自動生成(じどうせいせい)

// REST APIメソッド定義
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

type ApiRoute<Method extends HttpMethod, Path extends string, Body = never, Response = unknown> = {
  method: Method
  path: Path
  body: Body
  response: Response
}

// ルート定義
type Routes = {
  listUsers: ApiRoute<'GET', '/api/users', never, User[]>
  getUser: ApiRoute<'GET', '/api/users/:id', never, User>
  createUser: ApiRoute<'POST', '/api/users', Omit<User, 'id'>, User>
  updateUser: ApiRoute<'PUT', '/api/users/:id', Partial<User>, User>
  deleteUser: ApiRoute<'DELETE', '/api/users/:id', never, void>
}

// 型安全なクライアントを生成
type TypeSafeClient<R extends Record<string, ApiRoute<any, any, any, any>>> = {
  [K in keyof R]: R[K]['body'] extends never
    ? () => Promise<R[K]['response']>
    : (body: R[K]['body']) => Promise<R[K]['response']>
}

type Client = TypeSafeClient<Routes>

5. ブランデッド型(かた)とファントム型(かた)

問題(もんだい): 構造的(こうぞうてき)型付(かたづ)けの限界(げんかい)

TypeScriptは構造的(こうぞうてき)型付(かたづ)け(structural typing)を使用(しよう)します。構造(こうぞう)が同(おな)じであれば同(おな)じ型(かた)として扱(あつか)われます。

// 問題のシナリオ
type UserId = string
type PostId = string

function deleteUser(id: UserId): void {}
function deletePost(id: PostId): void {}

const userId: UserId = 'user-123'
const postId: PostId = 'post-456'

// どちらもstringなので、間違えて入れ替えてもエラーなし!
deleteUser(postId) // エラーが出るべきだが通過する
deletePost(userId) // これも通過する

解決策(かいけつさく): ブランデッド型(かた)

// ブランドシンボルを追加して構造的に区別
type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, 'UserId'>
type PostId = Brand<string, 'PostId'>
type OrderId = Brand<string, 'OrderId'>

// 作成関数
function createUserId(id: string): UserId {
  return id as UserId
}

function createPostId(id: string): PostId {
  return id as PostId
}

// これで型安全!
function deleteUser(id: UserId): void {}
function deletePost(id: PostId): void {}

const userId = createUserId('user-123')
const postId = createPostId('post-456')

deleteUser(userId) // OK
deleteUser(postId) // エラー!PostIdはUserIdに代入できない

通貨(つうか)型(かた)の安全性(あんぜんせい)

type Currency<C extends string> = number & { readonly __currency: C }

type USD = Currency<'USD'>
type EUR = Currency<'EUR'>
type JPY = Currency<'JPY'>

function usd(amount: number): USD {
  return amount as USD
}

function eur(amount: number): EUR {
  return amount as EUR
}

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

const price1 = usd(100)
const price2 = usd(50)
const priceEur = eur(80)

addUSD(price1, price2) // OK
addUSD(price1, priceEur) // エラー!EURはUSDに代入できない

データベースID型(かた)の分離(ぶんり)

// ジェネリックブランデッドIDシステム
type EntityId<Entity extends string> = Brand<string, Entity>

type UserId = EntityId<'User'>
type PostId = EntityId<'Post'>
type CommentId = EntityId<'Comment'>

// 型安全な関係定義
interface Post {
  id: PostId
  authorId: UserId
  title: string
  content: string
}

interface Comment {
  id: CommentId
  postId: PostId
  authorId: UserId
  text: string
}

// リポジトリでの活用
function findPostsByAuthor(authorId: UserId): Promise<Post[]> {
  return Promise.resolve([])
}

function findCommentsByPost(postId: PostId): Promise<Comment[]> {
  return Promise.resolve([])
}

// コンパイル時に誤ったID使用を防止
const userId = 'user-1' as UserId
const postId = 'post-1' as PostId

findPostsByAuthor(userId) // OK
findPostsByAuthor(postId) // エラー!

Zodとの結合(けつごう)

import { z } from 'zod'

// ZodスキーマにBranded Typesを適用
const UserIdSchema = z.string().uuid().brand('UserId')
type UserId = z.infer<typeof UserIdSchema>

const PostIdSchema = z.string().uuid().brand('PostId')
type PostId = z.infer<typeof PostIdSchema>

// ランタイム検証 + 型安全
const userId = UserIdSchema.parse('550e8400-e29b-41d4-a716-446655440000')

// 無効なフォーマットはランタイムでエラー
try {
  const invalid = UserIdSchema.parse('not-a-uuid')
} catch (e) {
  console.error('無効なUserId')
}

6. 型安全(かたあんぜん)パターン10選(せん)

パターン1: Discriminated Unions for State Machines

// 状態マシンを型で表現
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function renderUser(state: RequestState<User>): string {
  switch (state.status) {
    case 'idle':
      return '待機中...'
    case 'loading':
      return '読み込み中...'
    case 'success':
      return `ユーザー: ${state.data.name}`
    case 'error':
      return `エラー: ${state.error.message}`
  }
}

// 有限状態マシン
type TrafficLight =
  | { state: 'red'; timer: number }
  | { state: 'yellow'; timer: number }
  | { state: 'green'; timer: number }

function nextState(light: TrafficLight): TrafficLight {
  switch (light.state) {
    case 'red':
      return { state: 'green', timer: 30 }
    case 'green':
      return { state: 'yellow', timer: 5 }
    case 'yellow':
      return { state: 'red', timer: 30 }
  }
}

パターン2: neverによる網羅性(もうらせい)チェック

// すべてのケースが処理されているかコンパイル時に検証
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`)
}

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number }

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'rectangle':
      return shape.width * shape.height
    case 'triangle':
      return (shape.base * shape.height) / 2
    default:
      return assertNever(shape)
    // 新しいShapeを追加するとここでコンパイルエラー
  }
}

パターン3: 型安全(かたあんぜん)なイベントエミッター

type EventMap = Record<string, any>

class TypedEventEmitter<T extends EventMap> {
  private listeners: Partial<Record<keyof T, Function[]>> = {}

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = []
    }
    this.listeners[event]!.push(listener)
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach((fn) => fn(payload))
  }

  off<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    this.listeners[event] = this.listeners[event]?.filter((fn) => fn !== listener)
  }
}

// 使用
interface AppEvents {
  userLoggedIn: { userId: string; timestamp: Date }
  orderPlaced: { orderId: string; total: number }
  error: { message: string; stack?: string }
}

const emitter = new TypedEventEmitter<AppEvents>()

emitter.on('userLoggedIn', (payload) => {
  console.log(payload.userId) // 型安全: string
})

emitter.emit('orderPlaced', { orderId: '123', total: 99.99 }) // OK
emitter.emit('orderPlaced', { orderId: '123' }) // エラー: totalが不足

パターン4: 型蓄積(かたちくせき)によるBuilderパターン

// 必須フィールドを型レベルで追跡
type RequiredFields = 'host' | 'port' | 'database'

class ConnectionBuilder<Built extends string = never> {
  private config: Record<string, unknown> = {}

  host(value: string): ConnectionBuilder<Built | 'host'> {
    this.config.host = value
    return this as any
  }

  port(value: number): ConnectionBuilder<Built | 'port'> {
    this.config.port = value
    return this as any
  }

  database(value: string): ConnectionBuilder<Built | 'database'> {
    this.config.database = value
    return this as any
  }

  username(value: string): ConnectionBuilder<Built> {
    this.config.username = value
    return this as any
  }

  // すべての必須フィールドが設定された場合のみbuild可能
  build(this: ConnectionBuilder<RequiredFields>): Record<string, unknown> {
    return this.config
  }
}

// 使用
new ConnectionBuilder().host('localhost').port(5432).database('mydb').build() // OK

new ConnectionBuilder().host('localhost').port(5432).build() // エラー!databaseが不足

パターン5: Const Assertionsとreadonly Tuples

// as constでリテラル型を保持
const ROLES = ['admin', 'editor', 'viewer'] as const
type Role = (typeof ROLES)[number] // 'admin' | 'editor' | 'viewer'

// 設定オブジェクトに適用
const CONFIG = {
  api: {
    baseUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3,
  },
  features: {
    darkMode: true,
    notifications: false,
  },
} as const

type Config = typeof CONFIG

// ルーティングテーブル
const routes = {
  home: '/',
  about: '/about',
  users: '/users',
  userDetail: '/users/:id',
} as const

type RoutePath = (typeof routes)[keyof typeof routes]
// '/' | '/about' | '/users' | '/users/:id'

パターン6: Variadic Tuple Types

// タプル型の結合
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]

type AB = Concat<[1, 2], [3, 4]> // [1, 2, 3, 4]

// pipe関数の型
function pipe<A, B>(fn1: (a: A) => B): (a: A) => B
function pipe<A, B, C>(fn1: (a: A) => B, fn2: (b: B) => C): (a: A) => C
function pipe<A, B, C, D>(fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): (a: A) => D
function pipe(...fns: Function[]) {
  return (x: any) => fns.reduce((acc, fn) => fn(acc), x)
}

const transform = pipe(
  (x: string) => x.length,
  (x: number) => x * 2,
  (x: number) => x.toString()
)

const result = transform('hello') // '10'

パターン7: 型安全(かたあんぜん)なORMクエリ

// 型安全なWHERE句
type WhereClause<T> = {
  [K in keyof T]?: T[K] | { eq: T[K] } | { ne: T[K] } | { gt: T[K] } | { lt: T[K] } | { in: T[K][] }
}

type OrderBy<T> = {
  [K in keyof T]?: 'asc' | 'desc'
}

interface QueryOptions<T> {
  where?: WhereClause<T>
  orderBy?: OrderBy<T>
  limit?: number
  offset?: number
}

function findMany<T>(table: string, options: QueryOptions<T>): Promise<T[]> {
  return Promise.resolve([])
}

// 使用
interface Product {
  id: number
  name: string
  price: number
  category: string
}

findMany<Product>('products', {
  where: {
    price: { gt: 100 },
    category: { in: ['electronics', 'books'] },
  },
  orderBy: { price: 'desc' },
  limit: 10,
})

パターン8: 再帰型(さいきかた)(JSON、ツリー)

// JSON型
type Json = string | number | boolean | null | Json[] | { [key: string]: Json }

// 型安全なツリー構造
interface TreeNode<T> {
  value: T
  children: TreeNode<T>[]
}

// ネストされたオブジェクトのパス型
type NestedKeyOf<T extends object> = {
  [K in keyof T & string]: T[K] extends object ? K | `${K}.${NestedKeyOf<T[K]>}` : K
}[keyof T & string]

interface DeepObj {
  user: {
    profile: {
      name: string
      avatar: string
    }
    settings: {
      theme: string
    }
  }
}

type Paths = NestedKeyOf<DeepObj>
// 'user' | 'user.profile' | 'user.profile.name' | ...

パターン9: HKT(Higher-Kinded Types)エミュレーション

TypeScriptは高次(こうじ)カインド型(かた)を直接(ちょくせつ)サポートしていませんが、エミュレートできます。

// HKTインターフェース
interface HKT {
  readonly type: unknown
}

// Functorインターフェースのエミュレーション
interface Functor<F extends HKT> {
  map<A, B>(fa: Kind<F, A>, f: (a: A) => B): Kind<F, B>
}

// Kind型ヘルパー
type Kind<F extends HKT, A> = F extends { readonly type: unknown }
  ? (F & { readonly type: A })['type']
  : never

// Array Functor
interface ArrayHKT extends HKT {
  readonly type: Array<this['type']>
}

// Option Functor
type Option<A> = { tag: 'some'; value: A } | { tag: 'none' }

interface OptionHKT extends HKT {
  readonly type: Option<this['type']>
}

パターン10: Module AugmentationとDeclaration Merging

// 既存モジュールの拡張
declare module 'express' {
  interface Request {
    user?: {
      id: string
      role: string
    }
    requestId: string
  }
}

// インターフェースのマージ(Declaration Merging)
interface Window {
  __APP_CONFIG__: {
    apiUrl: string
    version: string
  }
}

// 既存のenumを拡張
enum Color {
  Red = 'RED',
  Blue = 'BLUE',
}

// namespaceによる拡張
namespace Color {
  export function fromHex(hex: string): Color {
    return Color.Red
  }
}

Color.fromHex('#ff0000') // OK

7. Zod + tRPC: ランタイム + コンパイルタイム型安全(かたあんぜん)

Zodスキーマでランタイム検証(けんしょう) + 型推論(かたすいろん)

Zodは「スキーマファースト」アプローチで、一(ひと)つのスキーマからランタイム検証(けんしょう)とTypeScript型(かた)の両方(りょうほう)を得(え)ることができます。

import { z } from 'zod'

// スキーマ定義 = 型定義 + ランタイム検証
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(['admin', 'user', 'moderator']),
  preferences: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
    language: z.string().optional(),
  }),
  createdAt: z.coerce.date(),
})

// 型の自動推論
type User = z.infer<typeof UserSchema>

// ランタイム検証
const result = UserSchema.safeParse(unknownData)
if (result.success) {
  console.log(result.data.name) // 型安全
} else {
  console.error(result.error.issues) // 詳細なエラー情報
}

Zod高度(こうど)なパターン

// Discriminated unions
const EventSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('click'),
    x: z.number(),
    y: z.number(),
  }),
  z.object({
    type: z.literal('keypress'),
    key: z.string(),
    modifiers: z.array(z.string()),
  }),
  z.object({
    type: z.literal('scroll'),
    deltaX: z.number(),
    deltaY: z.number(),
  }),
])

type AppEvent = z.infer<typeof EventSchema>

// transformで型変換
const DateFromString = z.string().transform((str) => new Date(str))
type ParsedDate = z.infer<typeof DateFromString> // Date

// 再帰スキーマ
type Category = z.infer<typeof CategorySchema>
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(CategorySchema),
  })
)

tRPC: フルスタック型安全(かたあんぜん)API

tRPCを使用(しよう)すると、別途(べっと)のコード生成(せいせい)なしにサーバー・クライアント間(かん)の完全(かんぜん)な型安全性(かたあんぜんせい)を確保(かくほ)できます。

// サーバールーター定義
import { initTRPC } from '@trpc/server'

const t = initTRPC.create()

const appRouter = t.router({
  user: t.router({
    getById: t.procedure.input(z.object({ id: z.string().uuid() })).query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } })
      return user
    }),

    create: t.procedure
      .input(
        z.object({
          name: z.string().min(2),
          email: z.string().email(),
          role: z.enum(['admin', 'user']),
        })
      )
      .mutation(async ({ input }) => {
        return await db.user.create({ data: input })
      }),

    list: t.procedure
      .input(
        z.object({
          page: z.number().int().min(1).default(1),
          limit: z.number().int().min(1).max(100).default(20),
          role: z.enum(['admin', 'user']).optional(),
        })
      )
      .query(async ({ input }) => {
        return await db.user.findMany({
          skip: (input.page - 1) * input.limit,
          take: input.limit,
          where: input.role ? { role: input.role } : undefined,
        })
      }),
  }),
})

export type AppRouter = typeof appRouter

React Query + tRPC統合(とうごう)

// クライアント側 — 完全な型安全
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';

const trpc = createTRPCReact<AppRouter>();

function UserProfile({ userId }: { userId: string }) {
  const userQuery = trpc.user.getById.useQuery({ id: userId });

  if (userQuery.isLoading) return <div>読み込み中...</div>;
  if (userQuery.error) return <div>エラーが発生しました</div>;

  // userQuery.dataの型はサーバーから自動推論される
  return <div>{userQuery.data.name}</div>;
}

function CreateUserForm() {
  const createUser = trpc.user.create.useMutation({
    onSuccess: (data) => {
      console.log(`ユーザー作成完了: ${data.name}`);
    },
  });

  const handleSubmit = (formData: FormData) => {
    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
      role: 'user',
    });
  };

  return <form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }}>...</form>;
}

End-to-end型安全(かたあんぜん)アーキテクチャ

// 全体アーキテクチャフロー:
// 1. Zodスキーマを定義(単一の真実の源泉)
// 2. tRPCルーターでスキーマを使用(サーバー)
// 3. クライアントで型を自動推論(フロントエンド)
// 4. ランタイム検証はZodが自動処理

// DBスキーマとの連携(Drizzle ORM例)
import { pgTable, varchar, integer, timestamp } from 'drizzle-orm/pg-core'

const users = pgTable('users', {
  id: varchar('id', { length: 36 }).primaryKey(),
  name: varchar('name', { length: 50 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  age: integer('age'),
  createdAt: timestamp('created_at').defaultNow(),
})

// Drizzleから型推論
type DbUser = typeof users.$inferSelect
type NewDbUser = typeof users.$inferInsert

// ZodスキーマとDB型の同期
const CreateUserInput = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email().max(255),
  age: z.number().int().min(0).optional(),
}) satisfies z.ZodType<Omit<NewDbUser, 'id' | 'createdAt'>>

8. TypeScriptパフォーマンス最適化(さいてきか)

tsconfig.json最適(さいてき)設定(せってい)

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true
  }
}

型(かた)インスタンス化(か)の深(ふか)さ制限(せいげん)

複雑(ふくざつ)な再帰型(さいきかた)はTypeScriptの型(かた)インスタンス化(か)の深(ふか)さ制限(せいげん)(デフォルト50)に引(ひ)っかかる可能性(かのうせい)があります。

// 悪い例: 無限再帰のリスク
type DeepNested<T, Depth extends number[] = []> = Depth['length'] extends 10
  ? T
  : { value: T; nested: DeepNested<T, [...Depth, 0]> }

// 良い例: 再帰の深さを制限
type MaxDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

type SafeDeepPartial<T, D extends number = 5> = [D] extends [never]
  ? T
  : T extends object
    ? { [K in keyof T]?: SafeDeepPartial<T[K], MaxDepth[D]> }
    : T

Project Referencesでビルド速度(そくど)を改善(かいぜん)

大規模(だいきぼ)なモノレポではProject Referencesを使用(しよう)すると変更(へんこう)されたプロジェクトのみ再(さい)ビルドされます。

{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ],
  "compilerOptions": {
    "composite": true,
    "incremental": true
  }
}

各(かく)パッケージのtsconfig.json:

{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true
  },
  "references": [{ "path": "../shared" }]
}

ビルドコマンド:

# プロジェクト参照に従ってビルド
tsc --build

# 変更されたもののみインクリメンタルビルド
tsc --build --incremental

# クリーンビルド
tsc --build --clean

SWC vs esbuildトランスパイラ比較(ひかく)

項目tscSWCesbuild
言語TypeScriptRustGo
型チェックありなしなし
速度(1000ファイル)約10秒約0.5秒約0.3秒
バンドリングなしあり(SWC Pack)あり
デコレータ完全サポート部分サポート制限的
推奨用途CI型チェックNext.js、JestVite、バンドリング
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "esbuild src/index.ts --bundle --outdir=dist",
    "dev": "tsx watch src/index.ts"
  }
}

9. 型体操(かたたいそう)(Type Challenges)実践(じっせん)

型体操(かたたいそう)はTypeScriptの型(かた)システムだけでロジックを実装(じっそう)する練習(れんしゅう)です。実践(じっせん)で複雑(ふくざつ)な型(かた)を扱(あつか)う際(さい)に大(おお)きな助(たす)けになります。

Easyレベル

// 1. MyPick実装
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// テスト
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>
// { title: string; completed: boolean }

// 2. MyReadonly実装
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 3. Tuple to Object
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [K in T[number]]: K
}

const tuple = ['tesla', 'model 3', 'model X'] as const
type TupleObj = TupleToObject<typeof tuple>

// 4. First of Array
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never

type A = First<[3, 2, 1]> // 3
type B = First<[]> // never

// 5. Length of Tuple
type Length<T extends readonly any[]> = T['length']

type L = Length<[1, 2, 3]> // 3

Mediumレベル

// 1. Deep Readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}

// 2. Chainable Options
type Chainable<T = {}> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<T & Record<K, V>>
  get(): T
}

declare const config: Chainable
const result = config
  .option('foo', 123)
  .option('name', 'hello')
  .option('bar', { value: 'world' })
  .get()

// 3. Trim
type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer R}` ? TrimLeft<R> : S

type TrimRight<S extends string> = S extends `${infer R}${' ' | '\n' | '\t'}` ? TrimRight<R> : S

type Trim<S extends string> = TrimLeft<TrimRight<S>>

type Trimmed = Trim<'  Hello World  '> // 'Hello World'

// 4. Replace
type Replace<S extends string, From extends string, To extends string> = From extends ''
  ? S
  : S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${R}`
    : S

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

// 5. Flatten
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : []

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

Hardレベル

// 1. Curry(簡略化バージョン)
type Curry<F> = F extends (...args: infer A) => infer R
  ? A extends [infer First, ...infer Rest]
    ? (arg: First) => Rest extends [] ? R : Curry<(...args: Rest) => R>
    : R
  : never

declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>

function add(a: number, b: number, c: number): number {
  return a + b + c
}

const curriedAdd = curry(add)
const result2 = curriedAdd(1)(2)(3) // number

// 2. String Parser(簡略化されたパスパーサー)
type ParsePath<S extends string> = S extends `${string}:${infer Param}/${infer Rest}`
  ? { [K in Param]: string } & ParsePath<Rest>
  : S extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {}

type Params = ParsePath<'/users/:userId/posts/:postId'>
// { userId: string } & { postId: string }

// 3. IsUnion
type IsUnion<T, U = T> = T extends U ? ([U] extends [T] ? false : true) : never

type A1 = IsUnion<string | number> // true
type A2 = IsUnion<string> // false

学習(がくしゅう)ロードマップと推薦(すいせん)資料(しりょう)

初級(しょきゅう)(1-2週間(しゅうかん))

  1. 基本型(きほんかた)、インターフェース、ジェネリクス基礎(きそ)
  2. Union Types、Intersection Types
  3. Type Guards、Narrowing

中級(ちゅうきゅう)(2-4週間(しゅうかん))

  1. Conditional Types、Mapped Types
  2. Template Literal Types
  3. ユーティリティ型(かた)の内部(ないぶ)実装(じっそう)の理解(りかい)

上級(じょうきゅう)(4-8週間(しゅうかん))

  1. 再帰型(さいきかた)、Variadic Tuple Types
  2. Branded Types、Phantom Types
  3. HKTエミュレーション
  4. type-challengesリポジトリの演習(えんしゅう)

マスター(継続(けいぞく))

  1. コンパイラAPI活用(かつよう)
  2. カスタムトランスフォーマーの作成(さくせい)
  3. Language Service Pluginの開発(かいはつ)

10. クイズ

Q1: NoInferユーティリティ型の主な目的は何ですか?

NoInferはジェネリック関数(かんすう)で特定(とくてい)のパラメータが型推論(かたすいろん)に参加(さんか)しないように制御(せいぎょ)するユーティリティ型(かた)です。これにより意図(いと)しない型(かた)の拡張(かくちょう)(widening)を防止(ぼうし)できます。例(たと)えば、デフォルト値(ち)パラメータがジェネリック型(かた)Tの推論(すいろん)に影響(えいきょう)を与(あた)えないようにする場合(ばあい)に使用(しよう)します。

Q2: Distributive Conditional Typesで分配を防止するにはどうすればよいですか?

Conditional Typesでユニオンの分配(ぶんぱい)を防止(ぼうし)するには、extendsの両側(りょうがわ)を角括弧(かくかっこ)で囲(かこ)みます。例(たと)えば、T extends any ? T[] : neverの代(か)わりに[T] extends [any] ? T[] : neverと記述(きじゅつ)すると、ユニオン型(かた)が個別(こべつ)に分配(ぶんぱい)されず全体(ぜんたい)として処理(しょり)されます。

Q3: Branded Typesが必要な理由は何ですか?

TypeScriptの構造的(こうぞうてき)型付(かたづ)けシステムでは、構造(こうぞう)が同(おな)じ型(かた)は互換性(ごかんせい)があります。例(たと)えば、UserIdとPostIdがどちらもstringであれば、誤(あやま)って入(い)れ替(か)えてもコンパイルエラーが発生(はっせい)しません。Branded Typesはユニークなブランドプロパティを追加(ついか)して、構造的(こうぞうてき)には同(おな)じだが論理的(ろんりてき)には異(こと)なる型(かた)を区別(くべつ)できるようにします。これによりコンパイル時(じ)に誤(あやま)ったID使用(しよう)を防止(ぼうし)できます。

Q4: tRPCが従来のREST APIと比較して提供する型安全性の利点は何ですか?

tRPCでは、サーバーで定義(ていぎ)したルーター型(かた)がクライアントで自動的(じどうてき)に推論(すいろん)されます。別途(べっと)のコード生成(せいせい)(codegen)プロセスなしに、サーバーの入出力(にゅうしゅつりょく)型(かた)がクライアントで完全(かんぜん)なオートコンプリートと型(かた)チェックを提供(ていきょう)します。Zodスキーマと組(く)み合(あ)わせると、ランタイム検証(けんしょう)も自動(じどう)で行(おこな)われ、End-to-end型安全性(かたあんぜんせい)を確保(かくほ)できます。

Q5: TypeScript Project Referencesの主な利点は何ですか?

Project Referencesは大規模(だいきぼ)なモノレポでビルドパフォーマンスを最適化(さいてきか)します。主(おも)な利点(りてん)は以下(いか)の通(とお)りです。第一(だいいち)に、変更(へんこう)されたプロジェクトのみを再(さい)ビルドするインクリメンタルビルドが可能(かのう)です。第二(だいに)に、プロジェクト間(かん)の依存(いぞん)関係(かんけい)を明示的(めいじてき)に宣言(せんげん)してビルド順序(じゅんじょ)を自動(じどう)管理(かんり)します。第三(だいさん)に、compositeオプションと併用(へいよう)すると各(かく)プロジェクトの宣言(せんげん)ファイルをキャッシュし、他(た)のプロジェクトから参照(さんしょう)する際(さい)にソースファイルを再(さい)パースする必要(ひつよう)がなくなります。


11. 参考資料(さんこうしりょう)

  1. TypeScript公式ドキュメント — Handbook
  2. TypeScript 5.4リリースノート
  3. TypeScript 5.5リリースノート
  4. TypeScript 5.6リリースノート
  5. TypeScript 5.7リリースノート
  6. type-challenges GitHub
  7. Zod公式ドキュメント
  8. tRPC公式ドキュメント
  9. Total TypeScript — Matt Pocock
  10. TypeScript Deep Dive — Basarat
  11. Effect-TS — 型安全な関数型プログラミング
  12. Drizzle ORM — 型安全なORM
  13. SWC公式ドキュメント
  14. esbuild公式ドキュメント
  15. TypeScriptパフォーマンスWiki
  16. Branded Types in TypeScript — Kent C. Dodds
  17. TypeScriptの型システムがチューリング完全である理由