2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#シニアエンジニアが TypeScript (Next.js) を学んで戸惑ったこと・納得したこと

2
Last updated at Posted at 2026-03-13

⚙️ 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
「君が誰かはどうでも良い。君は idname を持っているかい?」

C#のような 「名目的型付け(Nominal Typing)」 では、設計やプログラミングの順番がこんな感じになると思います。

✅ C#の思考フロー: クラス中心

  1. どんなデータを扱うか考える
  2. そのデータが何者なのか抽象化し、名前を決める(クラス化・Entity定義)
  3. そのクラスが持つべきプロパティやメソッドを定義する
  4. 各プロパティが文字、数値、日付なのか等も厳密に決める
  5. インターフェースや継承で正当性を保証する
  6. 処理の流れを考え、必要なクラスを利用する

※ 最初(静的)に 「何者なのか?(Who)」 を厳格に決める。


対して、「動的」型付け言語の拡張であるTypeScriptが採用する 「構造的型付け(Structural Typing)」 では、型はもっと「流動的」な存在でした。

✅ TSの思考フロー: Shape(その時、どんな形か?)中心

  1. 実装したい「処理の流れ」や「データの出口(API/UI)」を考える
  2. その場所で必要な Shape = データの形状(プロパティ)」 をその場で定義する
  3. 既存の型から必要な分だけを抜き出したり、合成したりして「今この瞬間」の型を作る

※ 必要な箇所で柔軟に 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「君が誰かはどうでも良い。君は idname を持っているかい?」

C#
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へ変換できません

TypeScript
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流
TypeScript
type User = {
  id: number;
  name: string;
  phone: string;
  age?: number;
};

const user1: User = { id: 1, name: "山田", phone: "090" };
// phone は使わない
const { id, name } = user1;
C#
int id = user1.Id;
string name = user1.Name;

✅ デフォルト値の統合

  • DBの COALESCE のように、値が存在しない場合のフォールバックを入り口で定義できます
TypeScript
const { id, name, age = 20 } = user1;

(2) Zod:データの「門番」と「浄化(Parse)」

DBスペシャリストとして最も感銘を受けたのがZodの概念です。
C#では int.TryParse()DataAnnotations を使って検証しますが、Zodは単なる検証(Validation)ではなく、 データの浄化(Parsing) を行います。

✅ 検証ではなく「浄化」

C#エンジニアなら、URLクエリパラメータを数値に変換する際、パース処理と分岐を書くはずです。
TS + Zodでは、これを「検問所のルール(スキーマ)」として宣言的に定義します。

C#
// URLから "?id=123" を受け取った場合
string rawId = Request.Query["id"];
if (int.TryParse(rawId, out int id)) {
    // ここでようやく「数値」として扱える
}
TypeScript
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では**「型そのものを計算して作り変える(集合演算)」**というアプローチを取ります。

TypeScript
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は、オブジェクトの「参照(メモリアドレス)」が変わったことを検知して画面を再描画します。中身だけを書き換えても検知されません。そのため、履歴を汚さず「一部だけ変えた新しいコピー」を生成する儀式が必要です。

TypeScript
// C# の params = current with { preset = "year" };
const updatedParams = { ...current, preset: "year" };

こう考えると React Hook の useState がなぜ、状態が変わる変数と、その値変更メソッドの定義が必要になるのか、とても良くわかります。

TypeScript
// 削除確認ダイアログの表示状態管理
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#の構文については、コメント欄にて有益な補足をいただいておりますので、ぜひそちらもご参照ください。

2
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?