- Authors

- Name
- Youngju Kim
- @fjvbn20031
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.json で experimentalDecorators: 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追加時にshapeがneverでなくなるため、コンパイルエラーが発生します。
クイズ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段階だけ展開します。Bはnumber[][]をArray<number[]>として見るためnumber[]になります。DはUnionに分配されFlatten<string> | Flatten<number[]> = string | numberとなります。
まとめ
TypeScriptの型システムは単なる糖衣構文ではありません。正しく使えば実行時エラーをコンパイル時に検出でき、IDEの自動補完やリファクタリング支援を最大限に活用できます。
重要なポイント:
- 型の安全性を維持するために
anyの代わりにunknownを使用 - ユーティリティ型とマップ型でDRYな型定義を実現
- 判別共用体で状態機械を安全にモデリング
- ジェネリクスで再利用可能な型安全なコードを記述
satisfies演算子で型の検証と推論を同時に実現