🎯 この記事について
筆者は元々C#のエンジニアですが、最近 TypeScript(React / Next.js)へシフト中です。
TypeScriptはかなり短縮された独自の表現が多いと感じており、コードを読むのに苦労しています。
C#であればどう書くのか?なぜTypeScript(以下TS)ではこう書くのか?
こうした疑問点を自分用の備忘録として書き貯めましたので、同じ境遇の方の参考になれば幸いです。
1.undefined(C#にない概念)
TSには null とは別に undefined という状態がある。
C# にはない概念で、TSを理解する上で最も根本的な違いの一つ。
| 値 | 意味 |
|---|---|
null |
「値がない」と明示的に代入した |
undefined |
そもそも存在しない・省略された |
✅ ① 存在の確認
| 値 | if (q) |
|---|---|
| undefined | false |
| null | false |
| "" | false |
| "abc" | true |
if (q)
// undefinedはC#に無いが、一番近いコード
if (!string.IsNullOrEmpty(q))
✅ ② 省略された引数
C# の ?(Nullable)と TS の ?(オプショナル)は意味が異なる。
| 書き方 | 意味 |
|---|---|
C# int?
|
値が null かも(プロパティ自体は必ずある) |
TS keyword?: string
|
プロパティ自体が存在しないかも(省略可能) |
// 関数定義
async function search(params: {
keyword?: string; // 省略可能
pageSize: number; // 必須
}) { ... }
// 呼び出し側
search({ keyword: "あいう", pageSize: 10 }); // OK
search({ pageSize: 10 }); // OK(keyword 省略 → undefined)
search({ keyword: "あいう" }); // コンパイルエラー(pageSize は必須)
C# のデフォルト値付き引数(string keyword = null)に近いが、省略時に undefined になる点が異なる。
省略された引数は undefined になるので、
if (keyword) 一発で undefined / null / "" をまとめて弾ける。
const { keyword, pageSize } = params;
if (keyword) { ... } // undefined / null / "" のいずれも falsy
✅ ③ 存在しない配列要素へのアクセス
C# では空配列の [0] アクセスは例外が飛ぶ。TSでは undefined が返るだけ。
const results = []; // 空配列
const first = results[0]; // undefined(例外にならない)
TSのこの概念により、11レコード目を取得できたか?を .lengthだけでなく
if (results[10]) の1行で確認できるし、存在しなくても例外は発生しない。
2.イミュータブルスタイル(TSとReactの基本思想)
TSおよびReactでは 「値を変えるのではなく、新しい値を作って返す」 のが基本スタイル。
const values = [1, 2, 3, 4];
// ❌ ミュータブル(元の配列を直接変える)
values[0] = 99;
// ✅ イミュータブル(新しい配列を作る)
const newValues = values.map((v, i) => (i === 0 ? 99 : v));
TS, React, Next.jsなどを勉強していると、頻出するパターン。
なんとなく読み飛ばしていても、実はレガシーなC#のミュータブルな
コーディングスタイルと、全く違うのでパラダイムシフトが必要。
TSでは変数を書き換えるのではなく、変換した新しい値を返す書き方が主流。
例えるなら SQL で update 文を一切使わない。
必ず insert で追加。
✅ なぜイミュータブルか
参照型(配列・オブジェクト)を直接変えると、同じ参照を持つ別の変数にも影響する。
const 太郎 = { id: 1, name: "太郎" };
let 次郎 = 太郎; // 参照コピー
次郎.id = 99;
console.log(太郎.id); // 99(意図せず元も変わる)
イミュータブルスタイルなら元は変わらない。
const 次郎 = { ...太郎, id: 99 }; // 新しいオブジェクト 太郎の全プロパティ+idは上書き
console.log(太郎.id); // 1(元のまま)
Reactの状態管理(useState)もこの思想が根底にあり、
直接値を変えるのではなく新しいオブジェクトを渡すことで再レンダリングが検知される。
3.コロンの意味
🟦 TS コロンを利用する記述方法が複数存在する
// ① 三項演算子
const x = 条件 ? 真の値 : 偽の値;
// ② 型注釈
const x: string = "hello";
function foo(a: number) {}
// ③ インターフェース・型定義
type Foo = { name: string; age: number };
// ④ オブジェクトのキー
const obj = { key: value };
4.クラスや型の定義
✅ 一番汎用的な定義の宣言
🅲️ C# クラスを定義し インスタンス化
class User {
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var 太郎 = new User {
Id = 1,
Name = "太郎",
Email = "taro@test.com",
};
🟦 TS 型を定義し 型へ代入(インスタンス化ではないので newが無い)
type User = {
id: number;
name: string;
email: string;
};
const 太郎: User = {
id: 1,
name: "太郎",
email: "taro@test.com",
};
✅ Union型(C#にない概念)
🟦 TS とり得る値のORを定義
(C# だと enum や継承で表現するもの)
type Status = "pending" | "approved" | "rejected";
const s: Status = "approved"; // この3つ以外は型エラー
✅ 型の合成(C#のインターフェース多重継承に近い)
🟦 TS 2つのtypeを合成する
type WithTimestamp = { createdAt: Date };
type UserWithTimestamp = User & WithTimestamp;
✅ 型から一部のOmit / Pick(C#には無い)
🟦 TS 型から不要な列を削除して新しい型を作る(Omit)
// emailを除いたUserを型として定義したい場合
type UserWithoutEmail = Omit<User, "email">;
// → クラスを新たに書かずに型だけ作れる
🟦 TS 必要な列だけを残して新しい型を作る(Pick)
// id と name だけ欲しい場合
type UserSummary = Pick<User, "id" | "name">;
捨てたいフィールドが少ない → Omit、残したいフィールドが少ない → Pick が読みやすい。
💡 idとnameだけを取り出すなら、C#でも LINQ の Select で簡単に取り出せる
(ただし匿名型なので再利用できない)
var result = users.Select(p => new { p.Id, p.Name }).ToList();
// result の要素は Id と Name しか持たない
// ただし resultの型ができたわけではない。匿名型。
// 型として定義して再利用できる
type UserSummary = Pick<User, "id" | "name">;
function display(user: UserSummary) { ... } // ← 型として使える
5.スプレッド構文 ...(レガシーなC#にない概念)
... は文脈によって意味が変わる。 右辺なら展開、左辺なら残りをまとめる。
✅ ① スプレッド(展開して撒く)
🟦 TS オブジェクトのコピー・マージ
const 太郎 = { id: 1, name: "太郎", email: "taro@test.com" };
// 浅いコピー(shallow clone)
const 太郎コピー = { ...太郎 };
// → { id: 1, name: "太郎", email: "taro@test.com" }
太郎と、太郎コピーの参照アドレスは別
ただし オブジェクトの各プロパティが指すアドレスは同じ
// 参照コピー
// これは太郎変数の参照アドレスをコピーしているだけ
// 太郎の参照コピー.id=2 とすると、 太郎.idも2に変わる
const 太郎の参照コピー = 太郎;
// 一部を上書きしてマージ
const 太郎更新 = { ...太郎, email: "new@test.com" };
// → { id: 1, name: "太郎", email: "new@test.com" }
// C# record の with 式に近い: 太郎 with { Email = "new@test.com" }
🟦 TS 配列のマージ
const a = [1, 2];
const b = [3, 4];
const c = [...a, ...b]; // → [1, 2, 3, 4]
// C# の Concat に近い
C# 12以降はコレクションに限り似た構文がある
🅲️ C# 12以降
int[] a = [1, 2];
int[] b = [3, 4];
int[] c = [..a, ..b];
-
C# の
..: 配列・List・Spanなどのコレクション専用で、[..a, ..b] という構文の中でのみ使える -
TSの
...: オブジェクト・配列どちらにも使えて、関数の引数展開にも使える
✅ ② レスト(残りをまとめて受け取る)
🟦 TS 分割代入で「残り全部」を受け取る
const { email, ...rest } = 太郎;
// email → "taro@test.com"
// rest → { id: 1, name: "太郎" }
✅ スプレッド構文のパターンの整理
| 書き方 | 読み方 | 意味 |
|---|---|---|
{ ...太郎 } |
「太郎をスプレッド」 | 中身を展開して撒く |
{ ...a, ...b } |
「a と b をマージ」 | 2つを合成する |
const { x, ...rest } |
「x を取り出して残りを rest」 | 残りをまとめる |
6.分割代入(Destructuring)(C#にない概念)
オブジェクトや配列から値を取り出して変数に代入する構文。
C# 7.0 のタプル分解(var (a, b) = tuple;)に近いが、 オブジェクトのプロパティ名指定 や デフォルト値 など機能が豊富。
✅ ① 基本
🟦 TS オブジェクトから取り出す
const 太郎 = { id: 1, name: "太郎", email: "taro@test.com" };
const { id, name } = 太郎;
// id → 1
// name → "太郎"
🟦 TS 配列から取り出す
const [first, second] = [10, 20, 30];
// first → 10
// second → 20
✅ ② デフォルト値
undefined のときだけデフォルトが効く。null には効かない点に注意。
🟦 TS 関数引数での params 展開
const { page = 1 } = params;
// params.page が undefined → 1
// params.page が 3 → 3
// params.page が null → null (デフォルト値は効かない)
✅ ③ ネスト
🟦 TS 深い階層から直接取り出す
const data = { user: { id: 1, name: "太郎" } };
const {
user: { name },
} = data;
// name → "太郎"
ネストが深いと読みにくくなる。2階層までを目安にする。
✅ ④ 残余(rest)
スプレッド構文の ...rest と同じ記法。「取り出した残り全部」を受け取る。
🟦 TS オブジェクトの残余
const { email, ...rest } = 太郎;
// email → "taro@test.com"
// rest → { id: 1, name: "太郎" }
🟦 TS 配列の残余
const [first, ...others] = [1, 2, 3, 4];
// first → 1
// others → [2, 3, 4]
✅ ⑤ 関数引数での分割代入
オプショナルな引数を含む場合のベストプラクティス。
受け取り側で分割代入して、それ以降の存在を確定させる。
?とundefinedの詳細は冒頭の「undefined(C#にない概念)」を参照。
🟦 TS 受け取り側
async search(params: { keyword?: string; pageSize: number }) {
const { keyword, pageSize } = params;
// keyword は string | undefined として確定(params.keyword を毎回書かずに済む)
}
⑤-a オプショナル引数のデフォルト値
undefined のままでは困る場合は、分割代入時にデフォルト値を当てる。
以降の型が string | undefined から string に確定する。
🟦 TS
async search(params: { keyword?: string; pageSize: number }) {
const { keyword = "未設定", pageSize } = params;
// keyword は string で確定(undefined の可能性が消える)
}
✅ ⑥ 配列のスキップ
カンマだけ書くと、その要素を読み飛ばせる。C# のディスカード _ に近い。
🟦 TS
const [, second, , fourth] = [1, 2, 3, 4];
// second → 2
// fourth → 4
✅ パターンまとめ
| 書き方 | 意味 | C# の近似 |
|---|---|---|
const { x, y } = obj |
プロパティを取り出す | なし(タプル分解は別物) |
const { x = 0 } = obj |
undefined 時のデフォルト値 |
obj.X ?? 0 に近い |
const { x, ...rest } = obj |
残りをまとめる | なし |
const [a, , c] = arr |
配列の要素スキップ |
_ ディスカードに近い |
fn({ x, y = 0 }: T) |
引数で分割代入 | なし |
💡 余談 引数? Props?
React/Next.jsを学習してると頻繁に Props という単語が出てきます。
引数と何が違うのか最初混乱しましたが、
・Reactコンポーネントの引数は常にオブジェクト1つのみ
(オブジェクトの中身はどれだけプロパティがあっても良い)
という独自ルールがあります。
この独自ルールで受け取る引数のことを Props と呼んでます。
・Reactコンポーネントの引数 → Props
・ただの関数の引数 → 引数
7.ブラケット記法(動的プロパティアクセス)
C# でプロパティ名を変数で指定するにはリフレクションが必要で大げさになる。
🅲️ C# リフレクション
string propertyName = "classValue";
var value = item.GetType().GetProperty(propertyName).GetValue(item);
🟦 TS ブラケット記法で1行
const propertyName = "classValue";
item[propertyName] // item.classValue と同じ
[] の中に文字列変数を渡すと、その変数の中身をプロパティ名として動的に解決する。
ドット記法 item.propertyName と書いてしまうと「propertyName という名前のプロパティ」として固定されるので別物になる。
item.propertyName // item に "propertyName" というプロパティを探す(意図と違う)
item[propertyName] // propertyName の中身 "classValue" でプロパティを探す(正しい)
8.型推論
C# の var と同じで「確実に推論できる場合は不要」というのが TS のスタンス。
| ケース | 型指定 |
|---|---|
| 変数に初期値を代入する | 省略可(推論が効く) |
| 関数の引数・戻り値 | 明示推奨 |
| テストデータ・外部スキーマに合わせたい | 明示推奨 |
| any になりそうな場合 | 必須 |
9.クラスの代入
✅ 特定のプロパティだけ書き換えて新しいクラスへ代入
なぜオリジナルのオブジェクトを直接変更しないのか?
・直接変更(ミューテーション)すると、どこから参照されているかによって
予期しない副作用が生じる
・Reactはオブジェクトの参照が変わったことをトリガにして再レンダリングする(useState)
→ 直接変更では参照が変わらないため、変更が検知されない
🅲️ レガシーなC# インスタンスの値を直接変更
class User {
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var 太郎 = new User {
Id = 1,
Name = "太郎",
Email = "taro@test.com",
};
太郎.Email = "taro@example.com";
🟦 TS イミュータブルな書き方が基本
type User = {
id: number;
name: string;
email: string;
};
const 太郎: User = {
id: 1,
name: "太郎",
email: "taro@test.com",
};
// 変数「太郎」を直接書き換えない
// 変数「太郎」を展開し email列だけ上書きして、新しい変数へセット
const fixed太郎 = { ...太郎, email: "taro@example.com" };
🅲️ C# 9以降(TypeScriptと同じイミュータブルな書き方ができる)
record User(int Id, string Name, string Email);
var 太郎 = new User(1, "太郎", "taro@test.com");
var fixed太郎 = 太郎 with { Email = "taro@example.com" };
✅ 一部のプロパティを削除した新しいクラスへ代入
なぜわざわざプロパティを削除するのか?
・APIのレスポンスに password が含まれているとする
・安全のために受けとった password を保持し続けたくない
→ password = null で上書きするよりも、プロパティそのものを削除してしまえば、user.password を参照するコードはコンパイルエラーで弾いてくれてより安全になる
class User {
public int Id { get; set; }
public string Name { get; set; }
public string Password { get; set; }
}
var 太郎 = new User {
Id = 1,
Name = "太郎",
Password = "testpass",
};
// ここでPassword列のないuser(太郎)が欲しい場合
class UserWithoutPassword {
public int Id { get; set; }
public string Name { get; set; }
}
var 太郎withoutPassword = new UserWithoutPassword {
Id = 太郎.Id,
Name = 太郎.Name,
};
type User = {
id: number;
name: string;
password: string;
};
const 太郎: User = {
id: 1,
name: "太郎",
password: "testpass",
};
// ここでpassword列のないuser(太郎)が欲しい場合
// TSでは分割代入で1行で書ける
const { password: _, ...太郎withoutPassword } = 太郎;
// 太郎withoutPassword は { id: 1, name: "太郎" }
// ※ "_" は「使わない変数」を示す命名慣習(C#のディスカードとは別物)
10.配列(List)への操作
| C# LINQ | JS 配列メソッド |
|---|---|
| .Any(x => ...) | .some(x => ...) |
| .All(x => ...) | .every(x => ...) |
| .Where(x => ...) | .filter(x => ...) |
| .Select(x => ...) | .map(x => ...) |
| .First(x => ...) | .find(x => ...) |
| .FirstOrDefault() | .find() (なければ undefined) |
| .Count() | .length |
11.JSX の条件付きレンダリング(C#にない制約から生まれた書き方)
JSX(JavaScript XML) の中では if 文が書けない。
JSX は式(値を返すもの)しか置けないため、文である if は構文エラーになる。
🅲️ C# Razor では @if でテンプレート内に条件分岐を書ける
<div>
@if (isLoading) {
<Loader2 />
}
</div>
🟦 TS JSX では if 文は書けない
// ❌ if文は書けない
return <div>if (isLoading) {<Loader2 />}</div>;
そのため 三項演算子 か && 演算子 を使う。
// ✅ 三項演算子(false のときは null を返して何も表示しない)
return <div>{isLoading ? <Loader2 /> : null}</div>;
// ✅ &&(false のとき短絡評価で右辺を評価しない → 何も表示しない)
return <div>{isLoading && <Loader2 />}</div>;
&& の方が短く書けるので React では慣用的に使われる。
「表示しない側に代替要素が不要な場合」は &&、「両方向に表示内容がある場合」は三項演算子、という使い分けが一般的。
<div>{count && <Loader />}</div>;のように左辺が 0 になる場合、
そのまま 0 が 描画されてしまうバグが有名なので注意
12.プロパティのバリデーション(データアノテーション → Zod)
C# ではプロパティに [属性] を付けてバリデーションルールを宣言する。
Zod では型の宣言にそのままチェーンで連結する。
🅲️ C# データアノテーション
class 商品 {
[Required(ErrorMessage = "必須")]
[StringLength(10, ErrorMessage = "10文字以内")]
public string 商品CD { get; set; }
[Required(ErrorMessage = "必須")]
public string 商品名 { get; set; }
[Range(0, 999999, ErrorMessage = "0〜999999で入力してください")]
public decimal 単価 { get; set; }
public string? 備考 { get; set; }
}
🟦 TS Zod
const 商品Model = z.object({
商品CD: z.string().min(1, "必須").max(10, "10文字以内"),
商品名: z.string().min(1, "必須"),
単価: z.coerce.number().min(0).max(999999, "0〜999999で入力してください"),
備考: z.string().optional().nullable(),
});
✅ よく使うアノテーションの対応表
| C# アノテーション | Zod | 対象 |
|---|---|---|
[Required] |
.min(1, "必須") |
string |
[StringLength(max)] |
.max(max) |
string |
[Range(min, max)] |
.min(min).max(max) |
number |
[MinLength(min)] |
z.array(...).min(min, "min件以上必要") |
配列(1:N) |
[MaxLength(max)] |
z.array(...).max(max, "max件まで") |
配列(1:N) |
[RegularExpression(pattern)] |
.regex(/pattern/) |
string |
[EmailAddress] |
.email() |
string |
プロパティが nullable(?) |
.optional().nullable() |
全型 |
| 独自バリデーション | .refine((v) => 条件, "エラーメッセージ") |
全型 |
13.Zod — 1つのスキーマから入力用・出力用の型を生成
C# の実務では、画面入力用(全項目 string)とロジック用(decimal 等)でクラスを2つ書くのが一般的だ。
🅲️ C# クラスを2つ書く
// ロジック用
class 商品 {
public string 商品CD { get; set; }
public string 商品名 { get; set; }
public decimal 単価 { get; set; }
public string? 備考 { get; set; }
}
// 画面入力用(全部 string で受ける)
class 商品InputModel {
[Required] public string 商品CD { get; set; }
[Required] public string 商品名 { get; set; }
public string 単価 { get; set; } // string のまま受ける
public string? 備考 { get; set; }
}
🟦 TS Zod なら1つのスキーマから両方の型を生成できる
const 商品Model = z.object({
商品CD: z.string().min(1, "必須"),
商品名: z.string().min(1, "必須"),
単価: z.coerce.number().min(0, "0以上で入力してください"),
備考: z.string().optional().nullable(),
});
// 入力時の型 — parse前(フォーム入力値はすべて string)
type 商品Input = z.input<typeof 商品Model>;
// 出力時の型 — parse後(単価が number に変換済み)
type 商品Output = z.output<typeof 商品Model>;
スキーマを1つ書くだけで、InputModel とロジック用クラスの両方が手に入る。
🚀 最後に
これからも開発で気になる記法を発見したら追記していく予定です。