動くコード図鑑技術記事現場の渡り方キャリア論すべての記事About
技術記事

C# の interface が分かる人が TypeScript の interface で戸惑う3つ — 構造的型付けと implements 不要の世界

バイブス父さん
現役の業務SE
2026年7月6日10 min read
C# の interface が分かる人が TypeScript の interface で戸惑う3つ — 構造的型付けと implements 不要の世界

みなさんこんにちは!ヒロポンです!

C# でずっとやってきて、TypeScript の案件に初めて入った時の話。interface を書いて、それを使うクラスに implements を…って、探したんですよ。

無いんです。

implements を書いてないのに、型が通ってる。「え、これで合ってるの??」って、しばらく固まりました。

C# の interface と TypeScript の interface。名前は同じなのに、中身の思想が違う。ここを知らずに C# の感覚で書くと、地味に足をすくわれます。

この記事は、TypeScript interface C# 出身者向けの橋渡し。C# の interface(公称型・明示 implements)を踏み台にして、TS の構造的型付け(structural typing)との違いを3つ、C# コードと TS コードを対で並べて翻訳していきます。

C# が分かってるなら、TS の interface は「差分」だけ押さえれば済みます。ゼロから覚え直す必要はないんですよね。

C# interface と TypeScript interface の対応マップ

まず全体像から。同じ interface でも、判定方式からして別物です。

C# interface と TypeScript interface の対応マップ。型の判定方式・implements の明示・実行時の存在・形が一致した別型の代入・合成の5観点で比較した表

この5行を、コードで1つずつ潰していきます。

違い1: implements は任意 — 形が合えば暗黙に互換

C# は 公称型(nominal typing)interface を実装するには : ILogger と明示して、宣言した名前で互換性が決まります。

interface ILogger { void Log(string msg); }

// C# は : ILogger を明示しないと互換にならない
class FileLogger : ILogger {
    public void Log(string msg) { /* 出力処理 */ }
}

ILogger logger = new FileLogger();   // OK(implements してるから)

TypeScript は 構造的型付け(structural typing)implements を書かなくても、プロパティとメソッドの形が一致すれば勝手に互換扱いになります。

interface Logger { log(msg: string): void; }

// implements Logger を書いてない
class FileLogger {
    log(msg: string): void { /* 出力処理 */ }
}

const logger: Logger = new FileLogger();   // OK(形が一致してるから通る)

C# 脳だと「implements 書いてないのに、なんで代入できるの??」となる。

ここが最初の壁です。

TS では implements書いてもいい。タイプミスを早期に検出できる利点はあります。ただ、必須じゃない。形さえ合えば通る、という発想に切り替えるのが第一歩です。

違い2: interface は実行時に消える — 型レベルだけの存在

C# の interface は実行時にも存在します。is ILogger で判定したり、リフレクションで型情報を取れる。実体があるんですね。

object x = new FileLogger();
if (x is ILogger logger) {     // 実行時に型判定できる
    logger.Log("hello");
}

TypeScript の interface型注釈だけの存在。コンパイルして JavaScript になった瞬間に消えます。だから実行時には影も形もない。

const x: unknown = new FileLogger();

// ❌ interface は実行時に存在しないので instanceof できない
// if (x instanceof Logger) { ... }   // これはコンパイルエラー

// ✅ 実行時に形を確かめたいなら、自分でプロパティの有無を見る
if (typeof (x as any).log === "function") {
    (x as Logger).log("hello");
}

C# の「型は実行時も生きてる」感覚のまま instanceof Logger と書くと、エディタに怒られます。ん?なんでや、ってなるやつ。いい感じに混乱するポイント。interface設計図であって実体じゃない、と覚えておくと腑に落ちます。

違い3: 型の合成が緩い — ダックタイピング的に通る

C# は interface を多重に implements して型を組み合わせます。あくまで「宣言した interface」が基準。

TypeScript は extends を複数つなげたり、交差型 & で型を合成できる。しかも構造で見るので、宣言してない型でも形が合えば通ります。いわゆるダックタイピング(「アヒルみたいに鳴けばアヒル」)的な世界。

interface Point { x: number; y: number; }

function distance(p: Point): number {
    return Math.sqrt(p.x * p.x + p.y * p.y);
}

// Point を名乗ってないオブジェクトでも、x と y を持ってれば通る
const data = { x: 3, y: 4, label: "A" };
distance(data);   // 5(余分な label は無視される)

dataPoint だなんて一言も言ってないのに、xy を持ってるから distance に渡せる。C# なら「Point 型じゃないからダメ」と弾かれるところ。

これが便利でもあり、後で説明する怖さの源でもあります。

ミニマム検証のハンズオン

小さく試すなら、tsc(TypeScript コンパイラ)か、ブラウザの TypeScript Playground で十分。こんな感じで手元でサッと確認できます。

interface Named { name: string; }

const a = { name: "田中", age: 30 };
const named: Named = a;   // OK(name を持ってるから通る・age は無視)

console.log(named.name);  // 田中

実行結果(構造が合えば代入が通る・tsc --strict + 実行で確認):

構造的型付けの実行結果:distance(data)が5、Named型にnameを持つオブジェクトを代入して田中が出る

これをコピペして、name を消すと型エラーになる。プロパティの有無だけで互換が決まるのを、目で確認できます。1分で「あ、こういうことか」が掴めます!!

ハマりポイント(実体験ベース)

① 構造が偶然一致して、意図しない代入が通ってしまう

正直に言うと、俺もこれで詰まりました。

構造的型付けは、「別物のはずの2つの型が、たまたま同じ形だと互換扱いになる」。便利な反面、これがバグの温床になります。

interface UserId { value: number; }
interface ProductId { value: number; }

const uid: UserId = { value: 100 };
const pid: ProductId = uid;   // ❌ 通ってしまう(どっちも { value: number })

UserIdProductId は意味が全然違うのに、形が同じだから代入が通る。C# の公称型なら名前が違うので弾かれますが、TS は通します。

対策は、わざと形を変えて区別する branded type(タグ付け)

interface UserId { value: number; _brand: "UserId"; }
interface ProductId { value: number; _brand: "ProductId"; }
// これで形が変わり、取り違えをコンパイル時に弾ける

② interface と type、どっちを使う?

TS には interface とよく似た type もあって、最初は迷います。ざっくりの線引きはこう。

  • interface: オブジェクトの形を定義する。extends で拡張、宣言のマージができる
  • type: ユニオン型(A | B)、交差型、プリミティブの別名など、もっと広い型表現に使う

「クラスやオブジェクトの形」なら interface、「型の組み合わせや別名」なら type。最初はこれで十分です。

俺の現場メモ

C# から TS に移った時、一番効いたのは「TS の型は、C# の型より"ゆるい約束"」と割り切ったこと。

C# の型は実行時まで守られる固い契約。TS の型は、コンパイル時に形をチェックするだけの紳士協定。実行時には消える。

この温度差を最初に飲み込むと、implements 探しで手が止まることも、instanceof で怒られることもなくなります。

C# でちゃんと型を設計してきた人なら、TS の型システムは「もう一つの流儀」として普通に乗りこなせます。土台はそのまま使えるので、身構えなくて大丈夫。こんな感じで、C# の貯金がそのまま効くんですよね。

まとめ

要点はこれだけ。

  • C# は公称型(名前で判定・implements 必須)、TS は構造的型付け(形で判定・implements 任意)
  • TS の interface は実行時に消える。instanceof できないので、形チェックは自分で書く
  • 構造が偶然一致すると意図しない代入が通る → branded type で区別する
  • オブジェクトの形は interface、型の組み合わせは type

C# の型設計の感覚は、TS でもそのまま資産になります。違いは「ゆるさ」だけ、と押さえれば移行はスムーズです。

TypeScript を業務で本格的に使うなら、型システムを体系的に解説した本を1冊持っておくと、この手の「C# と何が違う?」を調べる時間がまるっと浮きます。

よくある質問

Q1. TypeScript で implements は書かない方がいいですか?

任意ですが、クラスが特定の interface を満たすことを明示したいなら書いてOKです。implements を書くと、形がずれた時にクラス側でコンパイルエラーが出るので、タイプミスを早期に検出できる利点があります。ただし互換性自体は形で決まるので、無くても動きます。

Q2. 構造的型付けで型の取り違えが怖いです。防げますか?

branded type(タグ付き型)で防げます。{ value: number; _brand: "UserId" } のように区別用のプロパティを足すと、形が変わって別型として扱われ、取り違えをコンパイル時に弾けます。ID 系など「同じ形だが意味が違う型」に有効です。

Q3. C# の interface のデフォルト実装は TS にもありますか?

TS の interface 自体に実装は持てません。共通の実装を持たせたいなら、抽象クラス(abstract class)を使うか、関数として切り出すのが定番です。C# 8 の interface デフォルト実装に相当する直接の機能は無い、と考えておくと混乱しません。

次に読むべき記事

以上!

同じ implements 探しで固まった人いたら、どんどんシェア待ってるぜ!!


執筆者

バイブス父さん — 業務 SE 7 年 (SIer 正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SIer の正社員からフリーランスに転じ、 複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

この記事のコードと手順は ぜんぶ動作検証済み。 安心して現場で試してくれ。
バイブス父さん

現役の業務SE。C# / SQL Server 保守の現場から、コードも人もキャリアも全部書く。 実体験ベース。

運営者について