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 の基本:条件型で型を抽出
infer は extends 条件型内でのみ使用でき、推論される型変数を宣言します。
// 関数の戻り値の型を抽出
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: チームで型体操を普及させるには?
- 単純なユーティリティ型(Partial、Pick、Omit)から始める
- チームの型ユーティリティライブラリ(
types/utils.ts)を構築 - コードレビューで重要な関数に完全な型を要求
- JSON フォーマッター を使用して JSON データ構造を検証し、型定義との整合性を確保
Q5: satisfies と型注釈の違いは?
型注釈(:)は型を注釈型に拡大しますが、satisfies は検証のみを行い推論型を変更しません。リテラル型の精度を保持したい場合は satisfies を、明確な型制約が必要な場合は型注釈を使用してください。
💡 Base64 エンコードツール を使用して、型定義内のインラインデータを処理し、外部依存を削減できます。
ブラウザローカルツールを無料で試す →