- Published on
TypeScript 型システムの深淵 — Structural Typing、Generics、Conditional、satisfies、tsc Go ポート、Zod、tRPC まで (2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
"TypeScript was a humble goal: add types to JavaScript. Ten years later, it's become one of the most sophisticated practical type systems ever shipped." — Anders Hejlsberg (C# 首席アーキテクト、TypeScript 作者)
2012 年 10 月 1 日、Microsoft は「JavaScript for Application-scale Development」として TypeScript を公開した。反応は冷ややかで、「また Microsoft が JavaScript を支配する試みか」という冷笑が多かった。
13 年後の 2025 年、Stack Overflow 調査で TypeScript は最も愛される言語の 3 位。React、Vue、Angular、Next.js が既定言語として採用し、GitHub の新規 JS リポの 70% が TypeScript で始まる。Deno と Bun は TypeScript をネイティブ実行する。
本記事は「.ts ファイルを書く」から「TypeScript の深みを理解する」へ進む人のための地図だ。
1. Gradual Typing — 成功の方程式
なぜ他の試みは失敗したか
- CoffeeScript (2009) — 構文は良いが型なし
- Dart (2011) — Google、独自ランタイム要求で失敗
- Flow (2014) — Facebook、型のみ追加したが TS に敗れる
- Nim, ReasonML — ニッチに留まる
TypeScript が生き残った理由:
- 段階的導入 — ファイル単位で移行可能
- JavaScript スーパーセット — すべての JS が有効な TS
- コンパイル後は純粋な JS — ランタイム依存ゼロ
- 強力な推論 — 型注釈最小
- 巨大なコミュニティ — DefinitelyTyped 17 万+ パッケージ
「Soundness を捨てたから成功した」
- TypeScript は soundness (健全性) を意図的に放棄
any、型アサーション (as)、交差型を許容- 結果は 実用的
- Flow はより厳格だったが 実用性で負けた
2. Structural Typing (構造的型付け)
Nominal vs Structural
Nominal (Java, C#): 型の「名前」が一致しなければならない
class Point { int x, y; }
class Vec2 { int x, y; }
Point p = new Vec2(); // エラー! 別クラス
Structural (TypeScript, Go): 形 (shape) が一致すれば OK
interface Point { x: number; y: number }
interface Vec2 { x: number; y: number }
const p: Point = { x: 1, y: 2 } // OK
const v: Vec2 = p // OK、同じ shape
Duck Typing の静的版
「アヒルのように歩きアヒルのように鳴けばアヒル」— Python はランタイム、TypeScript は コンパイル時 にこれを行う。
利点
- 柔軟性 — ライブラリ間の型互換が容易
- リファクタリング — interface 名変更の影響小
- Mock テスト容易
欠点 — Branded Types の必要性
type UserId = string
type PostId = string
function getUser(id: UserId) { /* ... */ }
const postId: PostId = 'p123'
getUser(postId) // 構造が同じで OK... バグ!
解決:
type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }
const userId = 'u1' as UserId
const postId = 'p1' as PostId
getUser(postId) // エラー!
TypeScript 5.6 (2024) で Branded Types が公式パターンとして文書化された。
3. 推論の芸術
リテラル推論
let x = 'hello' // string
const y = 'hello' // "hello" (リテラル)
const z = { a: 1 } // { a: number } -- readonly ではない
const は 値そのもの を型に、let は 広げた型。
as const — 完全不変
const config = {
mode: 'production',
debug: false,
} as const
// { readonly mode: "production"; readonly debug: false }
関数推論
function map<T, U>(arr: T[], fn: (x: T) => U): U[] { /* ... */ }
const nums = [1, 2, 3]
map(nums, x => x * 2) // T=number, U=number を自動推論
制約付き推論
function pick<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'Kim', age: 20 }
pick(user, 'name') // string
4. Generics — 関数の抽象化
基本
function identity<T>(x: T): T { return x }
const n = identity(42) // number
const s = identity('hello') // string
制約 (Constraint)
interface HasLength { length: number }
function logLength<T extends HasLength>(x: T): T {
console.log(x.length)
return x
}
logLength('hello') // OK
logLength([1,2,3]) // OK
logLength(123) // エラー
デフォルト値
type Page<T = any> = {
items: T[]
total: number
}
Variance (共変・反変)
関数の 引数 は反変、戻り値 は共変。TypeScript は既定で bivariant だが、strictFunctionTypes で正しくなる。
5. Conditional Types — 型レベルの if
基本
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
infer — 型抽出
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() { return { id: 1, name: 'Kim' } }
type User = ReturnType<typeof getUser> // { id: number; name: string }
標準の ReturnType、Parameters、Awaited はすべてこのメカニズム。
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never
type A = ToArray<string | number>
// string[] | number[] (Union に分配される)
実戦 — 有用な組み合わせ
type NonNullable<T> = T extends null | undefined ? never : T
type Unwrap<T> = T extends Promise<infer U> ? U : T
6. Mapped Types — オブジェクト変換の力
基本
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
修飾子 — +?、-?、+readonly、-readonly
type Required<T> = { [K in keyof T]-?: T[K] }
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
Key Remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}
DeepPartial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
7. Template Literal Types (TS 4.1, 2020)
基本
type Greeting = `Hello, ${string}`
const g: Greeting = 'Hello, World' // OK
組み合わせ
type Color = 'red' | 'blue'
type Size = 'sm' | 'lg'
type Class = `${Color}-${Size}`
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"
パース — infer と結合
type ExtractRoute<T> =
T extends `/users/${infer Id}/posts/${infer PostId}`
? { id: Id; postId: PostId }
: never
Router の型安全性 (Next.js、Remix が内部で活用)。
有名な活用 — Tailwind IntelliSense
text-red-500、bg-blue-200 等の数千クラスを型で表現。Template literal types なしでは不可能。
8. satisfies — 2022 年の小さな革命
問題
type Config = { [key: string]: string | number }
const config = {
name: 'app',
port: 3000,
} as Config
config.name.toUpperCase() // エラー! string | number
as キャストは 具体型情報を失う。
satisfies (TS 4.9)
const config = {
name: 'app',
port: 3000,
} satisfies Config
config.name.toUpperCase() // OK、string として推論
config.port.toFixed(2) // OK、number として推論
「この型を満たすか検査せよ、ただし 具体型は保て。」
実例
type Themes = Record<string, { primary: string; secondary: string }>
const themes = {
light: { primary: '#fff', secondary: '#000' },
dark: { primary: '#000', secondary: '#fff' },
} satisfies Themes
as Themes なら string になるが、satisfies ならリテラル保持。
9. 型レベルプログラミング — Type Challenges
GitHub の type-challenges/type-challenges は TypeScript の型だけで解く問題集。
Pick の実装
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
}
価値
- 型システムの限界と能力の理解
- ライブラリレベルの型設計
- 脳の筋トレ
ただし プロダクションでの過度な型プログラミングは避けよ — コンパイル速度と可読性が落ちる。
10. モジュール解決の複雑さ
CommonJS vs ESM
// CommonJS
const mod = require('./foo')
module.exports = { /* ... */ }
// ESM
import mod from './foo'
export default { /* ... */ }
Node.js が両方サポート、TypeScript が間を取り持つのは 地獄。
module オプション
commonjs、esnext、node16/nodenext、preserve
moduleResolution
node、node16/nodenext、bundler(TS 5.0+)
2024 年の現実
- ライブラリ作者に dual package (CJS + ESM) の負担
exportsフィールドで条件分岐- ESM-only への流れ (Vite、Vitest、Remix)
11. tsc の限界と Go ポート (2025)
tsc の問題
- TypeScript で書かれている (dogfooding)
- 大規模プロジェクト: コンパイル 30 秒〜3 分
- LSP 応答: 1〜2 秒
- 開発体験のボトルネック
代替
- swc — Rust、約 20 倍速、型チェックなし
- esbuild — Go、100 倍速、型チェックなし
- Bun — Zig、ネイティブ実行
Anders の衝撃発表 (2025 年 3 月)
"We are porting the TypeScript compiler to Go."
- Anders Hejlsberg が直接主導
- 10 倍速コンパイル、8 倍速 LSP
- 並列化可能 (JS 単一スレッドから脱却)
- 互換性 100%
- 2026–2027 一般公開目標
なぜ Rust ではなく Go か?
- 「同等の性能、低い学習曲線、豊富な標準ライブラリ」
- GC あり — 既存アーキテクチャの移植が容易
- 「Rust なら 2 年余分にかかる」
意義
- 10 年来の tsc 最大の変化
- 巨大モノレポ (Slack、Airbnb、Microsoft) の DX 革命
12. ランタイム検証の進化 — Zod、ArkType、Valibot
問題 — 型はコンパイル時のみ
function parseUser(json: string): User {
return JSON.parse(json) // ランタイム型検証なし!
}
外部データ (API、フォーム、ファイル) は ランタイム検証 が必要。
Zod — 2020 年登場、現行標準
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
age: z.number().int().positive(),
})
type User = z.infer<typeof UserSchema>
const user = UserSchema.parse(externalData)
- スキーマ 1 つで型 + 検証
- React Hook Form、tRPC 標準統合
ArkType — 型そのままで検証
import { type } from 'arktype'
const User = type({
id: 'number',
email: 'email',
age: 'number > 0',
})
TypeScript 構文そのまま (文字列 DSL)。極めて高速。
Valibot — Zod のツリーシェイキング改善
import { object, string, email, number, minValue, pipe } from 'valibot'
const UserSchema = object({
email: pipe(string(), email()),
age: pipe(number(), minValue(0)),
})
個別 import → バンドル 70% 削減。
比較
| 観点 | Zod | ArkType | Valibot |
|---|---|---|---|
| 成熟度 | 高 | 中 | 中 |
| バンドル | 大 | 中 | 最小 |
| 性能 | 中 | 最高 | 高速 |
| 構文 | メソッドチェーン | TS DSL | 関数型 |
| エコシステム | 最大 | 成長中 | 成長中 |
13. tRPC — エンドツーエンド型安全性
アイデア
クライアント ──(型共有)── サーバ
GraphQL なしで フロントエンドがバックエンドの型をそのまま使う。
サーバ定義
import { z } from 'zod'
import { router, publicProcedure } from './trpc'
export const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return await db.users.find(input.id)
}),
})
export type AppRouter = typeof appRouter
クライアント利用
import { createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '../server/router'
const trpc = createTRPCProxyClient<AppRouter>({ /* ... */ })
const user = await trpc.getUser.query({ id: 1 })
// user の型が自動推論!
マジック
- ランタイムは HTTP 呼び出し
- 型は ビルド時に共有 (型のみ import)
id型が違えばコンパイルエラー- GraphQL の 80% の恩恵、0% の複雑さ
採用
- Next.js フルスタックで圧倒的
- Theo (t3-stack) 主導
- 2024 年から Remix、SvelteKit 統合
14. Effect-TS — 関数型の最前線
アイデア
Haskell の Effect system を TypeScript で。副作用、エラー、依存を 型 で管理。
import { Effect } from 'effect'
const getUser = (id: number): Effect.Effect<User, DbError, DbService> =>
Effect.gen(function*() {
const db = yield* DbService
return yield* db.findUser(id)
})
型の意味:
- 成功型: User
- エラー型: DbError (検査例外的)
- 要求サービス: DbService (DI)
利点
- 例外が型に見える — catch 漏れ防止
- 依存注入が自動
- リトライ、タイムアウト、キャッシュ が合成可能
欠点
- 学習曲線が急
- エコシステム小
- チーム全体の合意が必要
2024–2025 の拡散
- Discord、Vercel の一部チームが採用
- Effect Conference 開催
- 関数型 TS の事実上の標準化
15. 2025 年の学習順序
初級
- 基本型
- Interface / Type alias
- Union / Intersection
- Generic 基礎
- Utility types
中級
- Conditional Types
- Mapped Types
keyof、typeof、in- Template Literal Types
satisfies活用- Zod でランタイム検証
上級
infer高度活用- 再帰型
- Variance と関数型
- 型デバッグ
- ライブラリレベルの型設計
エキスパート
- Effect-TS
- tRPC フルスタック
- Type Challenges
- TypeScript Compiler API
16. アンチパターン TOP 10
any乱用 — 型システム無効化// @ts-ignore放置 — バグを隠す習慣as強制キャスト —satisfiesがほぼ常に優位- Enum 使用 —
as constunion のほうが安全 Function型 — 具体的なシグネチャを書く- 過度な型プログラミング — コンパイル遅延、可読性低下
- 型とランタイムの不一致 — Zod 等で検証
readonly忘れ — 不変の意図を型で表現- Interface と Type のルールなき混在 — チーム規約必須
@types/*のバージョン不一致 — ランタイムと揃える
17. TypeScript チェックリスト
- strict: true
- noUncheckedIndexedAccess
- ランタイム検証 (Zod/ArkType/Valibot)
-
asを最小化、satisfies優先 - Branded Types
- tsconfig paths
- moduleResolution: bundler (Vite/Next.js)
- isolatedModules
- CI で
tsc --noEmit - typescript-eslint
- 型カバレッジ測定
- DefinitelyTyped 貢献
終わりに — 言語ではなく道具
Anders Hejlsberg はインタビューでこう語った:
"TypeScript is not trying to be the perfect type system. It's trying to be the most useful type system for the largest number of JavaScript developers."
完璧な型安全性 (Haskell、Rust) と完全な自由 (動的言語) の間で、TypeScript は 実用主義の頂点 を占める。その柔軟性ゆえに最初は中途半端に見えるが、10 年積み上げた結果は 大型チームの生産性を最も劇的に引き上げる道具 だ。
2025 年の TypeScript:
- Go へポート され 10 倍速へ
- Zod 等のランタイム検証 とのタイト統合
- tRPC によるフルスタック型安全性
- Effect-TS で関数型フロンティア 探求
言語の革命はまだ終わっていない。
"TypeScript is what JavaScript could have been, if JavaScript had been designed with tooling in mind." — Daniel Rosenwasser (TypeScript PM)