⚙️ TypeScriptの正体: 動的型付け言語の「拡張」
C#を20年以上書いてきた僕が、初めて TypeScript(以下TS)に触れて抱いた違和感。
それは、TSがゼロから設計された「静的型付け言語」ではなく、
- 基本は「動的型付け言語(JavaScript)」
- 開発、コンパイル時の指標としてだけの型チェック機能を被せた拡張
だったことです。(悪いと言ってるわけじゃないです)
✅ 「静的」型付け vs 「動的」型付け
そもそも、両者には根本的な違いがあります。
(1) 「静的」型付け (C#やJavaなど):
- コンパイル時に変数の型が決定する
- 型安全性が高く、大規模システムでも堅牢に動く
- 「クラス定義」などの事前手続きが必要
(2) 「動的」型付け (JavaScriptなど):
- プログラムの実行中に代入される値によって型が決まる
- 事前の定義なしにサクサク書ける柔軟性がある
- 実行するまでバグに気づきにくいリスク(ランタイムエラー)が伴う
✅ TypeScriptの立ち位置
TSは、この 「自由でカオスな動的型付けの世界(JS)」に、開発時だけ「静的型付けの秩序」を被せたもの でした。
- JavaScript(素の姿): すべてが動的。変数には何でも入るし、オブジェクトはただの連想配列。
- TypeScript(仮の姿): JSに「構造的型付け」というメタデータを付与し、コーディング中やビルド時にエラーを検知する検問。
C#ではランタイム(実行時)でも型(身分)が保証されますが、TSの型はコンパイルが終わればただのJavaScriptになり、型情報はすべて消失します。だからこそ、TSは「名前(身分)」ではなく「形(持ち物)」でしかデータを判断できないのです。
❓ 名目的型付け と 構造的型付け
名目的型付け: C#
「君は User クラスのインスタンスかい?」
構造的型付け: TypeScript
「君が誰かはどうでも良い。君は id と name を持っているかい?」
C#のような 「名目的型付け(Nominal Typing)」 では、設計やプログラミングの順番がこんな感じになると思います。
✅ C#の思考フロー: クラス中心
- どんなデータを扱うか考える
- そのデータが何者なのか抽象化し、名前を決める(クラス化・Entity定義)
- そのクラスが持つべきプロパティやメソッドを定義する
- 各プロパティが文字、数値、日付なのか等も厳密に決める
- インターフェースや継承で正当性を保証する
- 処理の流れを考え、必要なクラスを利用する
※ 最初(静的)に 「何者なのか?(Who)」 を厳格に決める。
対して、「動的」型付け言語の拡張であるTypeScriptが採用する 「構造的型付け(Structural Typing)」 では、型はもっと「流動的」な存在でした。
✅ TSの思考フロー: Shape(その時、どんな形か?)中心
- 実装したい「処理の流れ」や「データの出口(API/UI)」を考える
- その場所で必要な 「
Shape= データの形状(プロパティ)」 をその場で定義する - 既存の型から必要な分だけを抜き出したり、合成したりして「今この瞬間」の型を作る
※ 必要な箇所で柔軟に 「Shape(どんな形をしているか)」 を決める。
C#のシニアエンジニアかつDBスペシャリストの視点からすると、
「なんで最初に厳密に型定義しないの?」
と最初は非常に戸惑いました。
しかし学習を深める中で 「必要な時に、必要な形を、柔軟に定義する」 というメンタルモデルへの切り替えが、フロントエンドのUI構築において極めて合理的であることが見えてきました。
🚧 強固な「城」を築くC# vs 受取り時に「検問」するTypeScript
両者では、 「データが型として安全である(べき)期間」 の設計思想が根本的に異なることを理解しました。
✅ C#:一度入れば安全な「城」のモデル
データの入り口(DBや型安全な通信)が強固に管理され、一度受け取ったらシステム内部での安全が担保されます。
- 信頼の連鎖: DBからEntityを引き出し、DTOやViewModelに詰め替える。
- 長い生存期間: 生成されたインスタンスは、複雑なビジネスロジックの中を「正しい構造」を維持したまま旅をする。
✅ TS:入り口が多く常にカオスな「検問所」のモデル
Next.jsなどのモダンフロントエンドが向き合うデータは、常に外部(ブラウザの入力、URL、外部API)からの不確かな存在ばかりです。
-
テキストが支配する世界: URLクエリ(?id=123)もJSONレスポンスも、本質はただの文字列や無機質なオブジェクト。TSはそこに無理やり「型」というラベルを貼っているに過ぎない。
(本質的には unknown を受け取ってる。だけどそれだとコーディングしづらい。) -
短い生存期間(エフェメラル): 入口から受け取り、即座に加工し、出口(UIコンポーネント)へ渡す。処理単位が短いため、重厚なクラス定義よりも「一時的な型定義(構造)」を量産する方が効率的。
🌊 「入口」と「出口」の距離感
C#エンジニアがTSで戸惑う「型定義の多さ」は、この「入口から出口までの距離の短さ」に起因していると思います。
-
C#の検証: 入口で一度「ガチガチの検証(バリデーション)」をすれば、あとは型を信頼してドメインロジックに集中できる。
-
TSのパース: どこから「汚染されたデータ」が紛れ込むかわからない。そのため、関数の入り口(分割代入)やZodによるパースなど、至る所に検問所を置く必要がある。
💡 なぜ「構造的型付け」なのか?
JSONという「名前のない構造体」を、クラスに変換するオーバーヘッドなしに直接扱うため。
🔄 C#とTSの具体的な違い
C#「君は User クラスのインスタンスかい?」
TS「君が誰かはどうでも良い。君は id と name を持っているかい?」
public class User {
public int Id { get; set; }
public string Name { get; set; }
}
public class Customer {
public int Id { get; set; }
public string Name { get; set; }
}
void Greet(User user) {
Console.WriteLine($"Hello, {user.Name}");
}
var customer = new Customer {Id = 1, Name = "太郎"};
Greet(customer); // ❌ エラー:CustomerからUserへ変換できません
type User = {
id: number;
name: string;
};
// 関数は「この形を満たしているか」だけを見る
// id: number、name: string さえ持っていれば良い
const greet = (user: User) => {
console.log(`Hello, ${user.name}`);
};
// その場で生成した「名もなきオブジェクト」でも通る
// (User typeとは一言も言っていない)
const unknownData = { id: 99, name: "Sato", email: "sato@example.com" };
greet(unknownData); // ✅ OK!
この柔軟性は 「裏を返せば、実行時に予期せぬデータが紛れ込むリスク」 です。
静的型付け言語に慣れている僕には超怖いです。
だからこそ、TypeScript のエコシステムではこれから解説する4つの武器を駆使して堅牢性を担保します。
(1) 分割代入(必要なものだけを「空中キャッチ」)
C#のような「オブジェクトを丸ごと受け取る」スタイルからの脱却です。
名前(クラス)で守られないなら、せめて「使うプロパティ」だけを明示的に取り出して安全なスコープに閉じ込める思想です。
✅ 必要なプロパティだけ取り出す
- 使わない変数は最初から宣言しないのがTS流
type User = {
id: number;
name: string;
phone: string;
age?: number;
};
const user1: User = { id: 1, name: "山田", phone: "090" };
// phone は使わない
const { id, name } = user1;
int id = user1.Id;
string name = user1.Name;
✅ デフォルト値の統合
- DBの COALESCE のように、値が存在しない場合のフォールバックを入り口で定義できます
const { id, name, age = 20 } = user1;
(2) Zod:データの「門番」と「浄化(Parse)」
DBスペシャリストとして最も感銘を受けたのがZodの概念です。
C#では int.TryParse() や DataAnnotations を使って検証しますが、Zodは単なる検証(Validation)ではなく、 データの浄化(Parsing) を行います。
✅ 検証ではなく「浄化」
C#エンジニアなら、URLクエリパラメータを数値に変換する際、パース処理と分岐を書くはずです。
TS + Zodでは、これを「検問所のルール(スキーマ)」として宣言的に定義します。
// URLから "?id=123" を受け取った場合
string rawId = Request.Query["id"];
if (int.TryParse(rawId, out int id)) {
// ここでようやく「数値」として扱える
}
import { z } from "zod";
// 1. 「検問所のルール(スキーマ)」を定義
const UserSchema = z.object({
id: z.coerce.number(), // 文字列の "123" を数値の 123 に浄化(強制変換)
name: z.string().min(1),
email: z.string().email().optional(),
});
// 2. 外部からの「汚染されてる可能性のあるデータ(不明なオブジェクト)」
const rawData = { id: "123", name: "Sato" };
// 3. 検問(パース)の実行
const result = UserSchema.safeParse(rawData);
if (result.success) {
// ここを通れば、dataは完全に「浄化」され、型安全な状態になる
const safeUser = result.data;
console.log(safeUser.id); // 123 (number型)
}
✅ C#でもセッターで細かくバリデートできるのでは?
違いその1:境界での一括浄化
- C#でWebAPIなどを作る場合、一度すべて文字列のクラスで受け取り、そこから安全なクラスへ詰め替えるという手間が発生しがち。
- TSとZodの場合、APIから飛んできた正体不明のJSONをスキーマに放り込むだけで、一発でバリデーションと型変換が完了し、安全に扱えるオブジェクトへ浄化される。
違いその2:関心事の分離
- C#のセッターにチェック処理を書くと、コードが肥大化しがち。
- Zodは、検問所のルールだけを独立して定義。
違いその3:エラーの全件収集のしやすさ
- C#のセッターでバリデーションをしようとすると、不正な値を代入した瞬間に例外を飛ばす設計が多い。(すごく頑張ればできなくはないけど、、、)
- ZodのsafeParseは、オブジェクト全体を一気に検査し、すべてのエラー項目をリストとしてまとめて一括で返してくれる。
- C#のセッター制御:「オブジェクト自身が内部を健全に保つための自己防衛(カプセル化)」
- Zod:「外部の予測不能な世界と、システム内部の安全地帯を隔てる検問所」
💡 違いの理解と納得ポイント
- C#では 型(class や struct)がまず存在し、そこにデータを流し込む
- TS + Zodでは 「スキーマ(ルール)」が先にあり、そこを通過したデータに後付けで「型」が宿る という逆転の発想になる
- z.infer のように、スキーマから型を自動生成(DRY原則)できるのも強力
(3) Omit / Pick(既存の「形」を削り出す)
C#で特定のプロパティを除外した「登録用DTO」が必要な場合、新しいクラスを定義し、AutoMapper等で詰め替えるのが一般的です。しかしTSでは**「型そのものを計算して作り変える(集合演算)」**というアプローチを取ります。
type User = {
id: number;
name: string;
email: string;
createdAt: Date;
};
// 1. Pick: 必要なものだけ「抽出」する(名前とメアドだけでいい時)
type UserContact = Pick<User, "name" | "email">;
// 2. Omit: 不要なものを「削る」(新規登録時は id や createdAt は不要)
type CreateUserDto = Omit<User, "id" | "createdAt">;
// 3. Partial: すべてを「任意(Optional)」にする(更新処理で一部の項目だけ送る時)
type UpdateUserDto = Partial<CreateUserDto>;
✅ C#との比較:DTOの量産 VS 型の加工
| 特徴 | C# (名目的型付け) | TypeScript (構造的型付け) |
|---|---|---|
| 新しい形が必要な時 | 新しいクラスを手書きで定義 | Omit/Pick等で既存の型から演算生成 |
| 変換コスト | コンストラクタやMapperでの移送が必要 | 構造が同じならそのまま代入可能 |
| メンテナンス | 元クラスを変えたら全DTOの修正が必要 | 元のTypeを変えれば派生した型も自動追従 |
「型 = 振る舞いの制限」ではなく、 「型 = プロパティの集合」 と捉えているTSならではの強み
(継承時のプロパティの追加はC#も得意。でもOmitして演算生成は無い)
(4) 「不変性(Immutability)」という最強の防御策
C#のオブジェクト指向では user.Name = "新太郎"; のような破壊的変更(ミューテーション)が日常的ですが、React/Next.jsの世界では厳禁です。
✅ スプレッド構文(...)による不変性の担保
Reactは、オブジェクトの「参照(メモリアドレス)」が変わったことを検知して画面を再描画します。中身だけを書き換えても検知されません。そのため、履歴を汚さず「一部だけ変えた新しいコピー」を生成する儀式が必要です。
// C# の params = current with { preset = "year" };
const updatedParams = { ...current, preset: "year" };
こう考えると React Hook の useState がなぜ、状態が変わる変数と、その値変更メソッドの定義が必要になるのか、とても良くわかります。
// 削除確認ダイアログの表示状態管理
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
🚀 まとめ: C#エンジニア向けのTSマインドセット
C#とTypeScriptは、どちらも静的型付けの恩恵を受けられますが、その「守り方」が違います。
-
「城」ではなく「検問」: 全体をクラスで固めるのではなく、データの境界線(API通信、フォーム入力、URL引数)でZodなどの検問を徹底する。
-
「誰か?」より「形状」: 「何者か(Class Name)」に固執せず、「何を持っているか(Properties)」でデータを扱う。
-
「詰め替え」より「削り出し」: 新しいDTOクラスを作る前に、Omit や Pick で今ある型を再利用・演算できないか考える。
-
「let」より「const」: 変数は基本的に再代入しない。
C#で培った「型への厳格さ」を、TSの「構造的な柔軟性」と組み合わせることができれば、モダンなフロントエンド開発において最強の武器になるはずです。
💡 思考のパラダイムシフト
| 観点 | C#(オブジェクト指向・名目的) | TS(関数型パラダイム・構造的) |
|---|---|---|
| データの持ち方 | class にデータと振る舞いを密結合(カプセル化) | type で形を定義し、純粋関数で変換する |
| 変更の作法 | インスタンスのプロパティを直接書き換える | スプレッド構文で「新しいコピー」を作る |
| nullへの構え | if (obj != null) で堅牢に守る | Optional Chaining (?.) で受け流す |
| 型の正体 | 実行時もメモリに存在する「身分証」 | コンパイル時に消滅する「静的な検問」 |
🍰 おまけ: アーキテクチャから見る役割分担
フロントエンドが受け取るデータは、本質的に不確実なJSONです。
フロントも通信もJSONなのだから、データベースも NoSQL にしてJSONのまま保存すれば変換の手間がなくて最強ではないか? という思想が流行したのも理解できます。
でも現実の業務システムでは、売上や在庫といった複雑なビジネスデータにおいて、厳密なリレーションやトランザクションが担保できないNoSQLは、データ整合性の維持はやはり難しいと思います。
適材適所で
- 柔軟なフロントエンド
- 堅牢なバックエンド
この組み合わせがやはり最強なんだと思います。
私自身もフロントエンド領域は探求中の身です。
C#エンジニアの方、TSエキスパートの方、何か補足や誤りがあればお気軽にコメントで教えてください🙇
補足:C#のモダンな構文について
本記事では、TypeScriptの構造的型付けのパラダイムを際立たせるため、あえてC#の伝統的なクラス定義や明示的な型宣言と比較しています。
現在のC#(Record型やC# 14の最新機能など)では、TypeScriptと同様に洗練された分割代入やnull制御、パターンマッチングなどが可能になっています。言語として「機能的にできること」は両者ともに進化し続けていますが、本記事ではそれぞれの言語エコシステムに根付く「デフォルトの文化や作法の違い」を感じていただければ幸いです。詳細なモダンC#の構文については、コメント欄にて有益な補足をいただいておりますので、ぜひそちらもご参照ください。