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

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- 1. TypeScript 5.x 新機能(しんきのう)(2024-2025)
- 2. ジェネリクス マスタークラス
- 3. Conditional Types 深掘り
- 4. Mapped TypesとTemplate Literal Types
- 5. ブランデッド型(かた)とファントム型(かた)
- 6. 型安全(かたあんぜん)パターン10選(せん)
- パターン1: Discriminated Unions for State Machines
- パターン2: neverによる網羅性(もうらせい)チェック
- パターン3: 型安全(かたあんぜん)なイベントエミッター
- パターン4: 型蓄積(かたちくせき)によるBuilderパターン
- パターン5: Const Assertionsとreadonly Tuples
- パターン6: Variadic Tuple Types
- パターン7: 型安全(かたあんぜん)なORMクエリ
- パターン8: 再帰型(さいきかた)(JSON、ツリー)
- パターン9: HKT(Higher-Kinded Types)エミュレーション
- パターン10: Module AugmentationとDeclaration Merging
- 7. Zod + tRPC: ランタイム + コンパイルタイム型安全(かたあんぜん)
- 8. TypeScriptパフォーマンス最適化(さいてきか)
- 9. 型体操(かたたいそう)(Type Challenges)実践(じっせん)
- 10. クイズ
- 11. 参考資料(さんこうしりょう)
はじめに
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.groupByとMap.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トランスパイラ比較(ひかく)
| 項目 | tsc | SWC | esbuild |
|---|---|---|---|
| 言語 | TypeScript | Rust | Go |
| 型チェック | あり | なし | なし |
| 速度(1000ファイル) | 約10秒 | 約0.5秒 | 約0.3秒 |
| バンドリング | なし | あり(SWC Pack) | あり |
| デコレータ | 完全サポート | 部分サポート | 制限的 |
| 推奨用途 | CI型チェック | Next.js、Jest | Vite、バンドリング |
{
"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週間(しゅうかん))
- 基本型(きほんかた)、インターフェース、ジェネリクス基礎(きそ)
- Union Types、Intersection Types
- Type Guards、Narrowing
中級(ちゅうきゅう)(2-4週間(しゅうかん))
- Conditional Types、Mapped Types
- Template Literal Types
- ユーティリティ型(かた)の内部(ないぶ)実装(じっそう)の理解(りかい)
上級(じょうきゅう)(4-8週間(しゅうかん))
- 再帰型(さいきかた)、Variadic Tuple Types
- Branded Types、Phantom Types
- HKTエミュレーション
- type-challengesリポジトリの演習(えんしゅう)
マスター(継続(けいぞく))
- コンパイラAPI活用(かつよう)
- カスタムトランスフォーマーの作成(さくせい)
- 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. 参考資料(さんこうしりょう)
- TypeScript公式ドキュメント — Handbook
- TypeScript 5.4リリースノート
- TypeScript 5.5リリースノート
- TypeScript 5.6リリースノート
- TypeScript 5.7リリースノート
- type-challenges GitHub
- Zod公式ドキュメント
- tRPC公式ドキュメント
- Total TypeScript — Matt Pocock
- TypeScript Deep Dive — Basarat
- Effect-TS — 型安全な関数型プログラミング
- Drizzle ORM — 型安全なORM
- SWC公式ドキュメント
- esbuild公式ドキュメント
- TypeScriptパフォーマンスWiki
- Branded Types in TypeScript — Kent C. Dodds
- TypeScriptの型システムがチューリング完全である理由