はじめに
この記事では、TypeScript の型を Go や Java の型と比較しながら整理します。
TypeScript、Go、Java はいずれも「型がある言語」として見えます。
ただし、型が何を守ってくれるのか、どの段階で効くのかはかなり違います。
特にずれやすいのは、TypeScript の型を Go や Java の型と同じ感覚で見てしまうことです。
この感覚のまま入ると、as の意味、オブジェクトの代入、実行時の安全性あたりで引っかかりやすくなります。
なお、この記事のコード例は説明のために単純化したサンプルです。
実務コードそのものではなく、型の見え方の違いを切り出すための最小例として読んでください。
先に結論
先に結論を書くと、違いの中心は次のとおりです。
- Go / Java は、TypeScript より実行時の型の存在感が強い
- TypeScript は、JavaScript に静的型チェックを足している
- TypeScript の型はコンパイル後の JavaScript には基本的に残らない
- そのため、TypeScript は Go / Java より「実行時の型安全」を広く守るわけではない
つまり、TypeScript の型はかなり便利ですが、守備範囲が違います。
Go や Java のように「型があるなら実行時もかなり守られる」と思って読むとずれます。
1. number と int と Integer は同じではない
最初に押さえておきたいのは、同じ「数値の型」でも意味がかなり違うことです。
TypeScript では、代表的な数値型は number です。
const count: number = 10;
const price: number = 12.5;
const invalid: number = NaN;
const infinite: number = Infinity;
この number は、実行時には JavaScript の number です。
整数と小数を同じ型で扱います。
さらに、NaN や Infinity も number として扱われます。
一方、Go の int は整数型です。
var count int = 10
var price float64 = 12.5
Java でも、int と double は別です。
int count = 10;
double price = 12.5;
さらに Java には Integer があります。
Integer count = 10;
これは int のラッパークラスです。
そのため、TypeScript の number を Go の int や Java の Integer と同じ感覚で見るのは危険です。
特に重要なのは次の点です。
- TypeScript の
numberは実行時には JavaScript の数値である -
NaNやInfinityもnumberに入る - Go / Java は整数と浮動小数点を別の型として扱う
- Java の
Integerはクラスであり、intとも性質が違う
2. null / undefined / nil / null は似ているようで違う
このあたりも、名前だけで見ると混乱しやすいです。
TypeScript には null と undefined があります。
let a: string | null = null;
let b: string | undefined = undefined;
ここでは「値がない」が 2 種類あります。
しかも、TypeScript の設定次第では扱いの厳しさも変わります。
Go は nil です。
var p *User = nil
var s []string = nil
ただし nil を入れられるのは、ポインタ、スライス、map、interface、関数、channel などです。
int や string に nil は入りません。
Java は null です。
String name = null;
ただし null を入れられるのは参照型だけです。
int のようなプリミティブ型には null を入れられません。
この違いを雑にまとめると次のとおりです。
- TypeScript は
nullとundefinedが分かれている - Go の
nilは「何にでも入る空値」ではない - Java の
nullは参照型のための値であり、プリミティブ型とは別世界である
3. オブジェクトの型は TypeScript がかなり特殊に見える
ここは TypeScript を Go / Java と同じ感覚で見たときに、かなりずれやすいところです。
TypeScript では、オブジェクトは「名前」より「形」で見られる場面が多いです。
type User = {
id: number;
name: string;
};
const user = {
id: 1,
name: "Alice",
extra: "これは余分な値"
};
const u: User = user;
console.log(u);
このコードは通ります。
実行すると、extra もそのまま残ったオブジェクトが出力されます。
これは TypeScript が構造的型付けを使っているからです。
必要なプロパティを持っていれば、代入できる場面があります。
ただし、ここは少し注意が必要です。
TypeScript には余剰プロパティチェックがあるため、オブジェクトリテラルをその場で直接代入すると弾かれることがあります。
type User = {
id: number;
name: string;
};
const u: User = {
id: 1,
name: "Alice",
extra: "これはエラー"
};
つまり TypeScript は、常にゆるいわけではありません。
ただし基準は「クラス名が一致するか」ではなく、「その形として扱ってよいか」です。
Go はもう少し複雑です。
interface には構造的な面がありますが、named type は別物として扱われます。
struct も定義した型として明示的に扱う感覚が強いです。
package main
import "fmt"
type User struct {
ID int
Name string
}
func main() {
u := User{
ID: 1,
Name: "Alice",
}
fmt.Printf("%+v\n", u)
}
Java はさらに名前ベースです。
どの class / interface なのかがはっきり効きます。
class User {
int id;
String name;
User(int id, String name) {
this.id = id;
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
User u = new User(1, "Alice");
System.out.println(u.name);
}
}
この違いを一言でまとめると次のとおりです。
- TypeScript は構造で見る
- Go は構造的な面もあるが、named type の区別がかなり強い
- Java は基本的に名前で見る
4. TypeScript の as は Go / Java の cast と同じではない
ここもかなり誤解されやすいです。
TypeScript の as は、実行時変換ではありません。
型チェック上の見え方を変えるための記法です。
const value = document.getElementById("app") as HTMLDivElement;
この as HTMLDivElement が、実行時に何か変換してくれるわけではありません。
コンパイル後の JavaScript には残りません。
Go の型変換は、もっと実体があります。
var n int = 10
var f float64 = float64(n)
これは値の型を変換しています。
Java も同様です。
プリミティブ型の cast は値の変換ですし、参照型の cast は実行時の型関係に依存します。
double price = 10;
int n = (int) price;
参照型でも cast は実行時に無関係ではありません。
安全でない cast は例外につながります。
このため、TypeScript の as を見て「Go や Java の cast と同じ」と考えると危ないです。
- TypeScript の
asは主に静的チェック上の話 - Go の変換は値の変換
- Java の cast は実行時の型関係とも結びつく
5. 実行時に型情報が残るか
ここが一番大きい違いです。
TypeScript は、型チェックが終わると基本的に型情報を消して JavaScript にします。
つまり、interface や type alias は実行時にはそのまま存在しません。
そのため、TypeScript だけを見ていると次のように思いやすいです。
-
User型だから実行時にもUserとして守られる -
as Userと書いたから安全になった
しかし実際にはそうではありません。
例えば、次のコードはTypeScript上は User として扱えます。
type User = {
id: number;
name: string;
};
const value = JSON.parse('{"id": "not number", "name": "Alice"}') as User;
console.log(value.id + 1);
as User と書いても、実際の id は文字列のままです。
as は値を検査したり、変換したりしません。
例えば API から受け取った値が本当に User の形かどうかは、別途バリデーションしないと分かりません。
一方で Go や Java は、TypeScript より実行時の型の存在感が強いです。
- Go は concrete type を実行時にも持ち、reflection で見られる
- Java は
instanceofや reflection が使える
もちろん Go / Java でも何でも防げるわけではありません。
ただ、TypeScript と比べると「実行時の型」の存在感はかなり強いです。
TypeScript が弱いという話ではない
ここは誤解しない方がよいです。
TypeScript が弱いのではありません。
役割が違います。
TypeScript が強いのは次のような場面です。
- JavaScript の補完やリファクタリングを強くしたい
- API や関数の入出力を静的に揃えたい
- フロントエンドや Node.js で大きなコードベースを安全に保ちたい
つまり、TypeScript は「JavaScript を大規模に安全に書くための静的チェック」としてかなり強いです。
ただし、次の期待を持つとずれます。
- Go / Java のように実行時まで型が守ってくれるはず
-
asで安全になったはず - interface が実行時にも存在するはず
どこで一番ハマりやすいのか
実務で一番ハマりやすいのは、外から入ってくるデータです。
例えば次のようなものです。
- HTTP リクエスト
- JSON
- localStorage
- フォーム入力
これらは、TypeScript の型注釈を書いただけでは安全になりません。
実際の値がその形かどうかは、実行時に確認する必要があります。
この意味では、TypeScript の型だけで守るのではなく、必要に応じてバリデーションと組み合わせることが重要です。
まとめ
TypeScript、Go、Java はどれも型を持つ言語ですが、型の役割は同じではありません。
- TypeScript の型は JavaScript に足される静的チェックである
- Go / Java は実行時にも型の世界を強く持っている
- TypeScript の
asは Go / Java の cast と同じではない - TypeScript は構造的型付けなので、オブジェクトの見え方がかなり違う
TypeScript の型は便利です。
ただし、Go や Java の型と同じ感覚で見るとずれます。
特に重要なのは、「TypeScript は型がある言語」ではあるが、「Go や Java と同じ意味で型が効く言語」ではない、という点です。
Go と Java の考え方の差分をもう少し広く見たい場合は、Java経験者がGoを触って最初に戸惑ったこと でも整理しています。
Go の型や公開範囲が Java とどう違って見えるかについては、GoのパッケージシステムをJavaと比較しながら理解する もあわせて読むとつながりやすいです。