Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 3 / 12·約 8 分·第25期 どの言語にもある仕組み

型を確かめるのは誰で、いつか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

同じ書き方でも、何が起きるかはその言語が約束した分しか決まっていません。今回は、型が合っているかを確かめる役を、どの言語が誰に割り当てているかを見ます。

まず結論

型の食い違いを見つける役は、言語ごとに違います。実行前のコンパイラか、実行中の演算か、それとも誰も見ないかです。

TypeScript の `as` と Python の型ヒントは、書いても実行時には誰も照合しません。静的型か動的型かは「型があるかどうか」の区別ではなく、「いつ検査するか」の区別です。

? 読む前に予測

`n * 2` を返す `twice(n: number)` に `"21" as unknown as number` を渡す a.ts を、tsc 5.9.3 の `--strict` で検査してから Node v24.21.0 で実行します。何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

型を見る役は3通りある

関数に number と書けば、数以外は入らないように思えます。しかし「入らない」を実際に確かめるのは、言語が指名した誰かです。その役は3通りに分かれます。

  • 実行前のコンパイラ: Go のコンパイラや tsc。食い違えばコードは動き出さない
  • 実行中の演算: Python の + は、文字列と整数を足そうとした時点で止まる
  • 誰も見ない: TypeScript の as と Python の型ヒント。書いた人の主張のまま残る

3つ目が、この回の中心です。TypeScript の文書は型アサーションをこう説明します。

Like a type annotation, type assertions are removed by the compiler and won’t affect the runtime behavior of your code.

Reminder: Because type assertions are removed at compile-time, there is no runtime checking associated with a type assertion. There won’t be an exception or null generated if the type assertion is wrong.

主張が誤っていても、例外も null も出ません。 後ろの処理がそのまま、食い違った値を受け取ります。

同じ「型が違う値」を、どこで誰が止めるか
ソースコード型注釈・as・名前つき型
実行前
コンパイラ・tscGo は Celsius + Fahrenheit を拒否
実行中
演算そのものPython の "1" + 1 は TypeError
誰も見ないas と型ヒントは照合されない
  • ・どの役がどの食い違いを見るかは、言語と処理系の約束で決まる
  • ・tsc が通しても、実行時に誰かが照合するとは限らない
具体実物

tsc が0エラーで通し、Node が 42 を出す

実演は Docker の aarch64 環境、TypeScript 5.9.3 と Node v24.21.0 です。

type User = { name: string; age: number };
const raw: unknown = JSON.parse('{"name":"kazu"}');
const u = raw as User;                 // コンパイラへの主張。実行時の検査は無い
console.log(typeof u.age, u.age + 1);  // undefined + 1
function twice(n: number): number { return n * 2; }
console.log(twice("21" as unknown as number));
type Point = { x: number }; type Meters = { x: number };
const p: Point = { x: 1 }; const m: Meters = p; // 構造が同じなら通る
console.log(m);
実演ログ抜粋(node と tsc の2項を連結、tsc --version の項は省略。[exit N] は終了コード)コマンド出力
--- $ node /w/a.ts (no flags)
undefined NaN
42
{ x: 1 }
[exit 0]
--- $ tsc --strict --noEmit --target es2022 /w/a.ts
[exit 0]

Node の出力が3行、tsc は --strict でもエラー0件で [exit 0] です。age を持たない値が User として通り、u.age + 1 は NaN になりました。

as unknown as と二段にしているのには理由があります。"21" as number と直接書くと、tsc は拒否します。

$ tsc --strict --noEmit(const n = "21" as number; の1行だけのファイル)コマンド出力
../tmp/w.ts(1,11): error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

「as は何でも通す」は言い過ぎです。重なりの無い変換は止められ、エラーメッセージ自身が unknown を経由する抜け道を案内しています。 経由すれば、tsc はもう何も言いません。

では Node は .ts をどう実行したのか。v24 の文書はこう書きます。

By default Node.js will execute TypeScript files that contains only erasable TypeScript syntax. Node.js will replace TypeScript syntax with whitespace, and no type checking is performed.

同じ文書の履歴では、型除去は v23.6.0 で既定になり、v24.12.0 で安定版になっています。実機でも process.features.typescript は strip でした。Node は TypeScript を理解して実行しているのではなく、型の構文を消して JavaScript として実行しています。 旧名の --experimental-strip-types も v24.21.0 でまだ受け付けます。

✓ ここまでの確認

Node v24.21.0 がフラグなしで .ts ファイルを実行できたとき、Node は型について何をしていますか。

抽象原理

動的型にも型はあり、静的型でも実行時は保証されない

ここから2つの通説が崩れます。

「動的型言語には型が無い」は誤りです。 Python の値はすべて型を持ち、演算のたびに実行時に確かめられます。静的か動的かは、検査がいつ行われるかの違いです。ただし型ヒントは、その検査の対象ではありません。

The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.

PEP 484 も、注釈は __annotations__ から読めるが no type checking happens at runtime だとし、別の型チェッカーを利用者が自分で走らせる前提を書きます。さらに Python will remain a dynamically typed language と明言しています。

「静的型なら実行時に食い違わない」も誤りです。 TypeScript の文書は、自分の型システムについてこう認めています。

TypeScript’s type system allows certain operations that can’t be known at compile-time to be safe. When a type system has this property, it is said to not be “sound”.

コンパイラが通したことは、「コンパイラが見た範囲で矛盾が無い」までしか意味しません。as や外から来た値のように、コンパイラが見ていない経路があります。

具体実物

Python は注釈を素通りし、演算で止まる

同じ twice を Python 3.14.7 で書きます。

def twice(n: int) -> int:
    return n * 2
print(twice("21"))
print(twice.__annotations__)
try:
    "1" + 1
except TypeError as e:
    print("TypeError:", e)
$ python a.py(Python 3.14.7, aarch64)コマンド出力
2121
{'n': <class 'int'>, 'return': <class 'int'>}
TypeError: can only concatenate str (not "int") to str

注釈は __annotations__ に int として残っています。残っているのに、誰も照合していません。 "21" * 2 は文字列の繰り返しとして正しい演算なので、2121 が返ります。一方、"1" + 1 は実行時の演算が拒否しました。Python は型を検査しています。検査の対象が注釈ではなく演算だ、というだけです。

実行前に止める側の例として、Go(go1.27.1)で基底型が同じ2つの型を足します。

type Celsius float64
type Fahrenheit float64
// main の中
var c Celsius = 100
var f Fahrenheit = 212
fmt.Println(c + 1.5)       // 型なし定数は通る
fmt.Println(c + f)         // 名前が違うので通らない
$ go run(main.go を m.go にコピーして実行。出力は生ログのまま)コマンド出力
# command-line-arguments
./m.go:14:14: invalid operation: c + f (mismatched types Celsius and Fahrenheit)

上のコードは抜粋で、14行目は元ファイルの行番号です。エラーはこの1件だけで、c + 1.5 は通っています。これは型なし定数の規則によるもので、構造で型を比べた結果ではありません。

✓ ここまでの確認

Python 3.14.7 で `def twice(n: int) -> int: return n * 2` に `"21"` を渡した結果と、その理由の組み合わせはどれですか。

⇄ 2つを見比べる
具体例 A

Go: 名前つき型は名前が違えば別の型

Celsius も Fahrenheit も基底型は float64 ですが、足すとコンパイルエラーでした。Go spec は名前つき型を、他のどの型とも異なると定めます。単位の取り違えを実行前に止められます。

go1.27.1 のコンパイルエラーコマンド出力
invalid operation: c + f (mismatched types Celsius and Fahrenheit)
具体例 B

TypeScript: 形が同じなら同じ型

Point と Meters はどちらも x: number だけを持つので、Point の値を Meters の変数にそのまま代入でき、tsc は exit 0 でした。名前で区別したつもりでも、形が同じなら取り違えは止まりません。

tsc --strict は exit 0、node の出力コマンド出力
{ x: 1 }

どちらも実行前に型を検査する処理系です。それでも、単位の取り違えを止められたのは片方だけでした。何が違ったのでしょうか。

「同じ型」の判定に、宣言した名前を使うか、メンバーの形を使うかを見てください。

この章のまとめ

  • 型の食い違いを見つける役は、コンパイラ・実行時の演算・誰も見ないの3通り。
  • TypeScript の as はコンパイル時に消え、実行時の検査は無い。直接の "21" as number は TS2352 で拒否されるが、as unknown as なら通る。
  • Node v24.21.0 は .ts を型の構文を空白に置き換えて実行し、型検査はしない。
  • Python の型ヒントは実行時に強制されない。ただし演算は実行時に型を確かめる("1" + 1 は TypeError)。動的型にも型はある。
  • 静的か動的かは検査の時点の違い。何を同じ型とみなすか(名前か構造か)は別の軸で、Go の名前つき型どうしは名前で区別、TypeScript は構造。
  • tsc が0エラーでも、それはコンパイラが見た範囲の話。TS 自身が sound でないと書いている。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

静的型付けと動的型付けを分ける軸として適切なのはどれですか。

用語集(3語)
型アサーション (`as`)
TypeScript で、式の型をコンパイラに主張する構文。コンパイル時に消え、実行時の検査は無い。重なりの無い型への直接の `as` は TS2352 で拒否されるが、`unknown` を経由すると通る。
型除去 (type stripping)
Node が .ts の型の構文を空白に置き換えて実行する方式。v23.6.0 で既定になり、v24.12.0 で安定版。型検査はしない。
名前的型付け / 構造的型付け
型の一致を、宣言された名前で判定するか、持っているメンバーの形で判定するか。Go の名前つき型は前者、TypeScript は後者。