Skip to content
Published on

TypeScript完全ガイド: 型システムから高度なパターンまで実践解説

Authors

TypeScript完全ガイド: 型システムから高度なパターンまで実践解説

TypeScriptはJavaScriptに静的型システムを追加した言語です。2026年現在、フロントエンド・バックエンドを問わず、ほとんどの大規模プロジェクトでTypeScriptが標準として採用されています。本ガイドでは基礎から高度なパターンまで、実務ですぐに活用できる内容を解説します。


1. TypeScript型システムの基礎

1.1 プリミティブ型

TypeScriptのプリミティブ型はJavaScriptと同じですが、明示的に宣言します。

// 基本プリミティブ型
const name: string = '田中太郎'
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const undef: undefined = undefined

// ES6+の型
const sym: symbol = Symbol('unique')
const big: bigint = 9007199254740991n

// 型推論 - 代入時にTypeScriptが自動的に型を推論
const inferredName = '田中太郎' // stringとして推論
const inferredAge = 30 // numberとして推論

1.2 配列とタプル

// 配列の型表記(2通り、どちらも同じ)
const nums1: number[] = [1, 2, 3]
const nums2: Array<number> = [1, 2, 3]

// 読み取り専用配列
const readonlyNums: readonly number[] = [1, 2, 3]
// readonlyNums.push(4)  // Error: readonlyにpushは存在しない

// タプル - 長さと各位置の型が固定
const pair: [string, number] = ['Alice', 30]
const triple: [string, number, boolean] = ['Bob', 25, true]

// オプショナルタプル要素
const optional: [string, number?] = ['Charlie']

// レストタプル
const rest: [string, ...number[]] = ['start', 1, 2, 3, 4]

1.3 Enum

// 数値列挙型(デフォルト: 0から開始)
enum Direction {
  Up, // 0
  Down, // 1
  Left, // 2
  Right, // 3
}

// 文字列列挙型
enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE',
}

// const enum - コンパイル時にインライン化され、バンドルサイズを最適化
const enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
  Pending = 'PENDING',
}

// const enumは値が直接置き換えられる
const currentStatus = Status.Active // コンパイル後: 'ACTIVE'

// 通常enumとconst enumの使い分け:
// - 実行時にenumオブジェクトを反復する必要がある場合: 通常enum
// - パフォーマンス重視・反復不要の場合: const enum

1.4 any vs unknown vs never vs void

この4つはTypeScriptで最も混同されやすい特殊な型です。

// any - 型チェックを完全に無効化(使用を避けることを推奨)
let anyVal: any = '文字列'
anyVal = 42 // OK
anyVal = true // OK
anyVal.nonExistent() // 実行時エラーの可能性あり、コンパイルエラーなし

// unknown - anyより安全な代替
let unknownVal: unknown = '文字列'
// unknownVal.toUpperCase()  // Error: 型チェックが必要
if (typeof unknownVal === 'string') {
  unknownVal.toUpperCase() // OK: 型ガード後は使用可能
}

// never - 絶対に発生しない値の型
function throwError(msg: string): never {
  throw new Error(msg)
}

// neverは網羅性チェックに有効
type Shape = 'circle' | 'square' | 'triangle'
function area(shape: Shape): number {
  switch (shape) {
    case 'circle':
      return Math.PI * 1
    case 'square':
      return 1
    case 'triangle':
      return 0.5
    default:
      // shapeがnever型でなければならない - 新しいShapeを追加するとコンパイルエラー
      const _exhaustive: never = shape
      throw new Error(`Unknown shape: ${_exhaustive}`)
  }
}

// void - 戻り値のない関数
function logMessage(msg: string): void {
  console.log(msg)
}

1.5 Type AliasとInterface

// Type Alias
type Point = {
  x: number
  y: number
}

type ID = string | number

// Interface
interface IPoint {
  x: number
  y: number
}

// 主な違い:
// 1. Interfaceは宣言マージ(Declaration Merging)が可能
interface IPoint {
  z?: number // 既存のIPointにzが追加される
}

// 2. Type Aliasはunion/intersection/primitiveなど全ての型に使える
type StringOrNumber = string | number
type WithTimestamp = Point & { createdAt: Date }

// 3. どちらもclassでimplements可能
class MyPoint implements IPoint {
  x = 0
  y = 0
}

// 使い分けの基準:
// - ライブラリ/SDKの公開API: Interface(宣言マージで拡張しやすい)
// - アプリ内部の型: Type Alias(より柔軟)
// - オブジェクト形状のみ: どちらでも可

1.6 リテラル型とテンプレートリテラル型

// リテラル型
type Direction = 'north' | 'south' | 'east' | 'west'
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6

const move = (dir: Direction) => console.log(dir)
move('north') // OK
// move('up')  // Error

// テンプレートリテラル型(TypeScript 4.1+)
type EventName = 'click' | 'focus' | 'blur'
type Handler = `on${Capitalize<EventName>}`
// 結果: 'onClick' | 'onFocus' | 'onBlur'

type CSSUnit = 'px' | 'em' | 'rem' | 'vw' | 'vh'
type CSSValue = `${number}${CSSUnit}`
// 例: '16px', '1.5em', '100vw' など

// 実用例: APIルートの型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type APIRoute = `${HttpMethod} ${Endpoint}`
// 結果: 'GET /users' | 'POST /posts' | ... など12通りの組み合わせ

2. オブジェクト型とユーティリティ型

2.1 オブジェクト型の高度な機能

interface User {
  readonly id: number // 読み取り専用
  name: string
  email?: string // オプショナルプロパティ
  [key: string]: unknown // インデックスシグネチャ
}

const user: User = { id: 1, name: 'Alice' }
// user.id = 2  // Error: readonly

2.2 組み込みユーティリティ型

interface Todo {
  id: number
  title: string
  completed: boolean
  description: string
}

// Partial<T> - 全プロパティをオプショナルに
type PartialTodo = Partial<Todo>

// Required<T> - 全プロパティを必須に
type RequiredTodo = Required<Partial<Todo>>

// Readonly<T> - 全プロパティを読み取り専用に
type ReadonlyTodo = Readonly<Todo>

// Pick<T, K> - 特定プロパティのみ選択
type TodoPreview = Pick<Todo, 'id' | 'title'>

// Omit<T, K> - 特定プロパティを除外
type TodoWithoutDescription = Omit<Todo, 'description'>

// Record<K, V> - キーと値のマップ型
type TodoMap = Record<number, Todo>

// Exclude<T, U> - UnionからUに該当する型を除去
type T1 = Exclude<string | number | boolean, boolean> // string | number

// Extract<T, U> - UnionからUに該当する型のみ抽出
type T2 = Extract<string | number | boolean, string | boolean> // string | boolean

// NonNullable<T> - nullとundefinedを除去
type T3 = NonNullable<string | null | undefined> // string

// ReturnType<T> - 関数の戻り値の型を抽出
function getUser() {
  return { id: 1, name: 'Alice' }
}
type UserReturn = ReturnType<typeof getUser> // { id: number; name: string }

// Parameters<T> - 関数の引数型をタプルで抽出
function createUser(name: string, age: number, active: boolean) {}
type CreateUserParams = Parameters<typeof createUser> // [string, number, boolean]

// ConstructorParameters<T> - コンストラクタの引数型を抽出
class ApiClient {
  constructor(
    public baseUrl: string,
    public timeout: number
  ) {}
}
type ClientParams = ConstructorParameters<typeof ApiClient> // [string, number]

2.3 マップ型

// 基本のマップ型
type Flags<T> = {
  [K in keyof T]: boolean
}

interface Config {
  darkMode: string
  notifications: string
  analytics: string
}

type ConfigFlags = Flags<Config>
// { darkMode: boolean; notifications: boolean; analytics: boolean }

// 修飾子の追加・削除
type Mutable<T> = {
  -readonly [K in keyof T]: T[K] // readonlyを削除
}

type AllRequired<T> = {
  [K in keyof T]-?: T[K] // オプショナル性を削除
}

// キーの再マッピング(TypeScript 4.1+)
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 }

2.4 条件型

// 基本の条件型
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false

// 分配的条件型(Unionに分配される)
type ToArray<T> = T extends any ? T[] : never
type C = ToArray<string | number> // string[] | number[]

// inferキーワード - 条件型の中で型を推論
type UnpackArray<T> = T extends Array<infer Item> ? Item : T
type D = UnpackArray<string[]> // string
type E = UnpackArray<number> // number(配列でないのでそのまま)

type UnpackPromise<T> = T extends Promise<infer R> ? R : T
type F = UnpackPromise<Promise<string>> // string

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

3. ジェネリクス

3.1 ジェネリック関数

// 基本のジェネリック関数
function identity<T>(value: T): T {
  return value
}

const str = identity<string>('hello') // string
const num = identity(42) // number(型推論)

// 複数の型パラメーター
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second]
}

// 配列の最初の要素を返す
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

3.2 ジェネリックインターフェースとクラス

// ジェネリックインターフェース
interface Repository<T> {
  findById(id: number): Promise<T | null>
  findAll(): Promise<T[]>
  create(item: Omit<T, 'id'>): Promise<T>
  update(id: number, item: Partial<T>): Promise<T>
  delete(id: number): Promise<void>
}

// ジェネリッククラス
class Stack<T> {
  private items: T[] = []

  push(item: T): void {
    this.items.push(item)
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1]
  }

  get size(): number {
    return this.items.length
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }
}

const stack = new Stack<number>()
stack.push(1)
stack.push(2)
console.log(stack.pop()) // 2

3.3 ジェネリック制約(extends)

// extendsによる型制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Alice', age: 30 }
const name = getProperty(user, 'name') // string
// getProperty(user, 'email')  // Error: 'email'はkeyof typeof userではない

// インターフェース制約
interface HasId {
  id: number
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id)
}

// 複数制約
interface Serializable {
  serialize(): string
}

function processAndSerialize<T extends HasId & Serializable>(item: T): string {
  return `${item.id}: ${item.serialize()}`
}

3.4 実践的なジェネリックパターン

// APIレスポンス型パターン
interface ApiResponse<T> {
  data: T
  status: number
  message: string
  timestamp: string
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
  }
}

// Repositoryパターンの実装
class UserRepository implements Repository<User> {
  private baseUrl = '/api/users'

  async findById(id: number): Promise<User | null> {
    const res = await fetch(`${this.baseUrl}/${id}`)
    if (!res.ok) return null
    return res.json()
  }

  async findAll(): Promise<User[]> {
    const res = await fetch(this.baseUrl)
    return res.json()
  }

  async create(item: Omit<User, 'id'>): Promise<User> {
    const res = await fetch(this.baseUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item),
    })
    return res.json()
  }

  async update(id: number, item: Partial<User>): Promise<User> {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item),
    })
    return res.json()
  }

  async delete(id: number): Promise<void> {
    await fetch(`${this.baseUrl}/${id}`, { method: 'DELETE' })
  }
}

4. 高度な型テクニック

4.1 Union型とIntersection型

// Union型
type StringOrNumber = string | number
type Nullable<T> = T | null
type Optional<T> = T | null | undefined

// Intersection型
interface Named {
  name: string
}

interface Aged {
  age: number
}

type Person = Named & Aged
const person: Person = { name: 'Alice', age: 30 }

// 便利なintersectionパターン
type WithTimestamp<T> = T & {
  createdAt: Date
  updatedAt: Date
}

4.2 判別共用体(Discriminated Union / Tagged Union)

// 各ケースに固有のリテラル型の判別子を使用
interface LoadingState {
  status: 'loading'
}

interface SuccessState<T> {
  status: 'success'
  data: T
}

interface ErrorState {
  status: 'error'
  error: Error
}

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState

// 判別子による型の絞り込み
function renderState<T>(state: AsyncState<T>): string {
  switch (state.status) {
    case 'loading':
      return 'ロード中...'
    case 'success':
      return `データ: ${JSON.stringify(state.data)}`
    case 'error':
      return `エラー: ${state.error.message}`
  }
}

// Reduxアクションパターン
type Action =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'RESET' }

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT':
      return state + action.payload
    case 'DECREMENT':
      return state - action.payload
    case 'RESET':
      return 0
  }
}

4.3 型ガード

// typeof型ガード
function processValue(value: string | number): string {
  if (typeof value === 'string') {
    return value.toUpperCase() // value: string
  }
  return value.toFixed(2) // value: number
}

// instanceof型ガード
class Cat {
  meow() {
    return 'ニャー'
  }
}

class Dog {
  bark() {
    return 'ワン'
  }
}

function speak(animal: Cat | Dog): string {
  if (animal instanceof Cat) {
    return animal.meow() // animal: Cat
  }
  return animal.bark() // animal: Dog
}

// in演算子型ガード
interface Fish {
  swim(): void
}

interface Bird {
  fly(): void
}

function move(animal: Fish | Bird): void {
  if ('swim' in animal) {
    animal.swim() // animal: Fish
  } else {
    animal.fly() // animal: Bird
  }
}

// ユーザー定義型ガード(isキーワード)
function isString(value: unknown): value is string {
  return typeof value === 'string'
}

function isUser(obj: unknown): obj is User {
  return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj
}

// アサーション関数(TypeScript 3.7+)
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`stringを期待しましたが、${typeof value}を受け取りました`)
  }
}

4.4 オーバーロードシグネチャ

// 関数オーバーロード
function process(x: number): number
function process(x: string): string
function process(x: number[]): number[]
function process(x: number | string | number[]): number | string | number[] {
  if (typeof x === 'number') return x * 2
  if (typeof x === 'string') return x.toUpperCase()
  return x.map((n) => n * 2)
}

const r1 = process(10) // number
const r2 = process('hello') // string
const r3 = process([1, 2, 3]) // number[]

5. クラスとデコレーター

5.1 アクセス修飾子とクラス機能

class BankAccount {
  readonly accountNumber: string
  private _balance: number
  protected owner: string

  constructor(accountNumber: string, owner: string, initialBalance = 0) {
    this.accountNumber = accountNumber
    this.owner = owner
    this._balance = initialBalance
  }

  get balance(): number {
    return this._balance
  }

  private validateAmount(amount: number): void {
    if (amount <= 0) throw new Error('金額は0より大きくなければなりません')
  }

  deposit(amount: number): void {
    this.validateAmount(amount)
    this._balance += amount
  }

  withdraw(amount: number): void {
    this.validateAmount(amount)
    if (amount > this._balance) throw new Error('残高不足')
    this._balance -= amount
  }

  static createSavingsAccount(owner: string): BankAccount {
    const accountNumber = `SAV-${Date.now()}`
    return new BankAccount(accountNumber, owner, 0)
  }
}

// コンストラクタパラメーター短縮記法(パラメータープロパティ)
class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}

  distance(other: Point): number {
    return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2)
  }
}

5.2 抽象クラス

abstract class Shape {
  abstract area(): number
  abstract perimeter(): number

  // 共通実装
  toString(): string {
    return `面積: ${this.area().toFixed(2)}, 周長: ${this.perimeter().toFixed(2)}`
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super()
  }

  area(): number {
    return Math.PI * this.radius ** 2
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius
  }
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    super()
  }

  area(): number {
    return this.width * this.height
  }

  perimeter(): number {
    return 2 * (this.width + this.height)
  }
}

5.3 デコレーター

デコレーターを使用するには tsconfig.jsonexperimentalDecorators: true を設定する必要があります。

// クラスデコレーター
function Singleton<T extends new (...args: any[]) => {}>(constructor: T) {
  let instance: InstanceType<T>
  return class extends constructor {
    constructor(...args: any[]) {
      if (instance) return instance
      super(...args)
      instance = this as any
    }
  }
}

@Singleton
class AppConfig {
  readonly version = '1.0.0'
}

// メソッドデコレーター
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  descriptor.value = function (...args: any[]) {
    console.log(`呼び出し: ${propertyKey}(${JSON.stringify(args)})`)
    const result = original.apply(this, args)
    console.log(`戻り値: ${JSON.stringify(result)}`)
    return result
  }
  return descriptor
}

// プロパティデコレーター
function Validate(min: number, max: number) {
  return function (target: any, propertyKey: string) {
    let value: number
    Object.defineProperty(target, propertyKey, {
      get: () => value,
      set: (newValue: number) => {
        if (newValue < min || newValue > max) {
          throw new Error(`${propertyKey}${min}から${max}の範囲でなければなりません`)
        }
        value = newValue
      },
    })
  }
}

6. モジュールシステムと宣言ファイル

6.1 TypeScriptでのESモジュール

// 名前付きエクスポート
export interface User {
  id: number
  name: string
}

export function createUser(name: string): User {
  return { id: Date.now(), name }
}

// デフォルトエクスポート
export default class UserService {
  private users: User[] = []

  add(user: User): void {
    this.users.push(user)
  }
}

// 型のみのインポート(ランタイムでは削除される)
import type { User as UserType } from './user'

6.2 宣言ファイル(.d.ts)

// globals.d.ts - グローバル型の宣言
declare global {
  interface Window {
    analytics: {
      track(event: string, properties?: Record<string, unknown>): void
    }
  }
}

// モジュール宣言(型のないJSライブラリを補完)
declare module 'legacy-lib' {
  export function doSomething(value: string): number
  export const VERSION: string
}

// ファイル型の宣言(例: SVGファイル)
declare module '*.svg' {
  const content: string
  export default content
}

6.3 tsconfig.jsonの主要オプション

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "paths": {
      "@/*": ["./src/*"]
    },
    "outDir": "./dist",
    "declaration": true,
    "sourceMap": true,
    "esModuleInterop": true
  }
}

7. 実践パターン

7.1 メソッドチェーンによるBuilderパターン

class QueryBuilder<T> {
  private conditions: string[] = []
  private orderByClause?: string
  private limitValue?: number
  private offsetValue?: number

  where(condition: string): this {
    this.conditions.push(condition)
    return this
  }

  orderBy(field: keyof T & string, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.orderByClause = `${field} ${direction}`
    return this
  }

  limit(n: number): this {
    this.limitValue = n
    return this
  }

  offset(n: number): this {
    this.offsetValue = n
    return this
  }

  build(): string {
    let query = 'SELECT * FROM table'
    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`
    }
    if (this.orderByClause) {
      query += ` ORDER BY ${this.orderByClause}`
    }
    if (this.limitValue !== undefined) {
      query += ` LIMIT ${this.limitValue}`
    }
    if (this.offsetValue !== undefined) {
      query += ` OFFSET ${this.offsetValue}`
    }
    return query
  }
}

// 使用例
const query = new QueryBuilder<User>()
  .where('age > 18')
  .where('active = true')
  .orderBy('name')
  .limit(10)
  .offset(0)
  .build()

7.2 Result型(Either Monadパターン)

type Ok<T> = { success: true; data: T }
type Err<E> = { success: false; error: E }
type Result<T, E = Error> = Ok<T> | Err<E>

function ok<T>(data: T): Ok<T> {
  return { success: true, data }
}

function err<E>(error: E): Err<E> {
  return { success: false, error }
}

// 使用例
async function fetchUser(id: number): Promise<Result<User>> {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      return err(new Error(`HTTP ${response.status}`))
    }
    const data = await response.json()
    return ok(data)
  } catch (e) {
    return err(e instanceof Error ? e : new Error('Unknown error'))
  }
}

// 呼び出し元で明示的なエラー処理が強制される
const result = await fetchUser(1)
if (result.success) {
  console.log(result.data.name)
} else {
  console.error(result.error.message)
}

7.3 ブランド型(型安全なID)

// ブランド型 - 同じ基盤型でも互いに非互換
type UserId = number & { readonly _brand: 'UserId' }
type PostId = number & { readonly _brand: 'PostId' }
type CommentId = number & { readonly _brand: 'CommentId' }

// ファクトリ関数
function asUserId(id: number): UserId {
  return id as UserId
}
function asPostId(id: number): PostId {
  return id as PostId
}

function getUser(id: UserId): User {
  return {} as User
}

const userId = asUserId(1)
const postId = asPostId(1)

getUser(userId) // OK
// getUser(postId)  // Error: PostIdはUserIdに代入不可
// getUser(1)       // Error: numberはUserIdに代入不可

7.4 satisfies演算子(TypeScript 4.9+)

// satisfies - 型を検証しながら推論された型を維持
const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
} satisfies Record<string, string | number[]>

// satisfiesのおかげで:
palette.red.map((x) => x * 2) // OK: number[]として推論
palette.green.toUpperCase() // OK: stringとして推論

// 型アサーション不要で正しい型が維持される
const config = {
  port: 3000,
  host: 'localhost',
  features: {
    auth: true,
    logging: false,
  },
} satisfies {
  port: number
  host: string
  features: Record<string, boolean>
}

8. クイズ

クイズ1: unknownとany

以下のコードでコンパイルエラーが発生する行を選んでください。

const a: any = 'hello'
const b: unknown = 'world'

console.log(a.toUpperCase()) // (1)
console.log(b.toUpperCase()) // (2)

if (typeof b === 'string') {
  console.log(b.toUpperCase()) // (3)
}

正解: (2)の行

解説: unknown型は型ガードなしにプロパティやメソッドに直接アクセスできません。anyは型チェックを完全に無効化するため(1)はOKですが、unknownは型の絞り込み後にのみ使用できるため(2)はコンパイルエラーです。(3)はtypeof型ガード後なのでOKです。

クイズ2: ユーティリティ型の組み合わせ

以下の型をユーティリティ型で表現してください。

interface Article {
  id: number
  title: string
  content: string
  author: string
  publishedAt: Date
  updatedAt: Date
}

// 目標: id, publishedAt, updatedAtを除外し、残り全フィールドをオプショナルにした型
type ArticleUpdateInput = /* ??? */

正解: Partial<Omit<Article, 'id' | 'publishedAt' | 'updatedAt'>>

解説: まずOmitで編集不可のフィールドを除去し、Partialで残りを全てオプショナルにします。ユーティリティ型はこのように組み合わせて使用できます。

クイズ3: ジェネリック制約

以下の関数がコンパイルエラーなく動作するようにジェネリック制約を追加してください。

function mergeObjects<T, U>(target: T, source: U): T & U {
  return { ...target, ...source }
}

// 要件: TとUはともにobject型でなければならない

正解: function mergeObjects<T extends object, U extends object>(target: T, source: U): T & U

解説: extends objectを使うと基本型(string、numberなど)の渡し込みを防ぎ、オブジェクト型のみを許可します。

クイズ4: 判別共用体の網羅性チェック

以下のコードで新しい図形Triangleを追加してもコンパイルエラーが発生しない理由と、エラーを発生させるための修正方法を説明してください。

type Shape = Circle | Square // Triangle追加後: Circle | Square | Triangle

function getArea(shape: Shape): number {
  if (shape.kind === 'circle') return Math.PI * shape.radius ** 2
  if (shape.kind === 'square') return shape.side ** 2
  return 0 // この行によりコンパイルエラーなし
}

正解: return 0の代わりに const _: never = shape; throw new Error(...) パターンを使用

解説: return 0は全ての未処理ケースを静かに処理します。never網羅性チェックパターンを使うと、Triangle追加時にshapeneverでなくなるため、コンパイルエラーが発生します。

クイズ5: inferと条件型

以下の型の結果を予測してください。

type Flatten<T> = T extends Array<infer Item> ? Item : T

type A = Flatten<string[]> // ?
type B = Flatten<number[][]> // ?
type C = Flatten<string> // ?
type D = Flatten<string | number[]> // ?

正解: A = string, B = number[], C = string, D = string | number

解説: Flattenは配列を1段階だけ展開します。Bnumber[][]Array<number[]>として見るためnumber[]になります。DはUnionに分配されFlatten<string> | Flatten<number[]> = string | numberとなります。


まとめ

TypeScriptの型システムは単なる糖衣構文ではありません。正しく使えば実行時エラーをコンパイル時に検出でき、IDEの自動補完やリファクタリング支援を最大限に活用できます。

重要なポイント:

  • 型の安全性を維持するためにanyの代わりにunknownを使用
  • ユーティリティ型とマップ型でDRYな型定義を実現
  • 判別共用体で状態機械を安全にモデリング
  • ジェネリクスで再利用可能な型安全なコードを記述
  • satisfies演算子で型の検証と推論を同時に実現