04. 型の基本 06. any・unknown・型アサーション 資料トップへ戻る
TypeScript は型の名前を見ません。持っているプロパティの形だけを見ます。
type User = { name: string };
type Animal = { name: string };
const a: User = { name: "Alice" };
const b: Animal = a; // 通る。形が同じだから
User と Animal はまったく別の意図で作った型ですが、形が同じなので互換です。
判定の基準は「受け取る側が要求するものを、渡す側が持っているか」です。余分に持っていても通ります。
type Named = { name: string };
const user = { name: "Alice", age: 30, email: "a@example.com" };
const n: Named = user; // 通る。name を持っているから
Java・C# から来た人へ
これは Java・C# の名前的型付け(nominal typing)と根本的に違います。Java では、形が同じでも implements を書かなければ互換になりません。
TypeScript が構造的型付けを採用しているのは、JavaScript のコードに後から型を付けるためです。既存の JavaScript は「この型を実装します」とは宣言していません。形だけを見る方式なら、宣言を足さずに型を付けられます。
type Handler = (x: number) => void;
const f: Handler = (x) => console.log(x); // OK
const g: Handler = () => console.log("hi"); // OK — 引数を使わなくてよい
const h: Handler = (x: number, y: number) => {}; // エラー — 引数が多い
引数は少ない分には構いません。JavaScript では、渡された引数を使わない関数は普通に書けるためです([1,2,3].map(x => x * 2) のように、map が渡す2つ目・3つ目の引数を無視できます)。この扱いは 07. 関数の型 で詳しく見ます。
前の節で「余分に持っていても通る」と書きましたが、例外があります。
type User = { name: string };
// ① 変数を経由する → 通る
const obj = { name: "Alice", age: 30 };
const a: User = obj;
// ② オブジェクトリテラルを直接書く → エラー
const b: User = { name: "Alice", age: 30 };
// ~~~~~~~
// error TS2353: Object literal may only specify known properties,
// and 'age' does not exist in type 'User'.
//
// 訳: オブジェクト リテラルは既知のプロパティのみ指定できます。
// 'age' は型 'User' に存在しません。
同じ形なのに、①は通り、②はエラーになります。これが余剰プロパティチェックです。
打ち間違いを検出するためです。
構造的型付けでは「多い分には構わない」ため、{ namae: "Alice" } のような打ち間違いも「余分なプロパティ」として通ってしまいます。それでは型を書く意味がありません。
そこで「その場で書いたオブジェクトリテラル」に限って、余分なプロパティをエラーにします。変数を経由する場合は、書き手が意図して渡していると見なして通します。
type User = { name: string };
// ① 変数に入れてから渡す(最も素直)
const payload = { name: "Alice", age: 30 };
const a: User = payload;
// ② 型を union にする
type UserWithExtra = User & { age?: number };
// ③ インデックスシグネチャを足す
type Loose = { name: string; [key: string]: unknown };
as User で黙らせることもできますが、打ち間違いも一緒に通してしまいます(06)。
interface Logger {
log(message: string): void;
}
// implements を書いていない
class ConsoleLogger {
log(message: string) {
console.log(message);
}
}
const l: Logger = new ConsoleLogger(); // 通る
Java・C# から来た人へ
Java なら class ConsoleLogger implements Logger と書かなければコンパイルが通りません。TypeScript では書かなくても通ります。
ただし implements を書く意味はあります。書いておくと、クラス側の実装が要件を満たさなくなったときに、クラスの定義位置でエラーになります。書かない場合、エラーが出るのは「代入しようとした場所」で、原因から離れた位置になります。
class ConsoleLogger implements Logger { // 書いておくと、ここで守られる
log(message: string) { console.log(message); }
}
クラスに private なメンバーがあると、同じ形でも互換になりません。
class A { private secret = 1; }
class B { private secret = 1; }
let a: A = new A();
let b: B = new B();
a = b;
// ~
// error TS2322: Type 'B' is not assignable to type 'A'.
// Types have separate declarations of a private property 'secret'.
//
// 訳: 型 'B' を型 'A' に割り当てることはできません。
// プライベート プロパティ 'secret' の宣言が別々です。
private なメンバーは「そのクラスで宣言されたもの」として区別されます。これが、TypeScript で名前的な区別に最も近い挙動です。
構造的型付けの弱点は、意味が違うのに形が同じ値を取り違えられることです。
type UserId = string;
type OrderId = string;
function getUser(id: UserId) { /* ... */ }
const orderId: OrderId = "order-123";
getUser(orderId); // 通ってしまう。どちらも string だから
実体のないプロパティを足して、形を意図的にずらします。
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function getUser(id: UserId) { /* ... */ }
const orderId = "order-123" as OrderId;
getUser(orderId);
// ~~~~~~~
// error TS2345: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'.
//
// 訳: 型 'OrderId' の引数を型 'UserId' のパラメーターに割り当てることはできません。
__brand は実行時には存在しません。
型の上でだけ存在する目印です。実際の値はただの文字列なので、実行時のコストはゼロです。as を使わないと作れないため、「ここで意図的に変換した」という箇所が検索できるという副次的な利点もあります。
as を書く場所を1か所に閉じ込めると、検証も一緒に行えます。
// branded.ts
type UserId = string & { readonly __brand: "UserId" };
function toUserId(s: string): UserId {
if (!s.startsWith("user-")) {
throw new Error(`UserId の形式が不正です: ${s}`);
}
return s as UserId;
}
const id = toUserId("user-001");
console.log(id);
try {
toUserId("order-123");
} catch (e) {
console.log((e as Error).message);
}
$ npx tsx branded.ts
user-001
UserId の形式が不正です: order-123
この形は A2. 型レシピ集 にも収めています。
使いどころを絞ってください。
すべての string をブランド型にすると、変換だらけで読みにくくなります。取り違えると実害が出るもの(ID の種類、検証済み/未検証の文字列、単位の違う数値)に限って使います。
まとめ
| 項目 | この章の結論 |
|---|---|
| 判定の基準 | 名前ではなく形。受け取る側が要求するものを持っていれば通る |
| 余分なプロパティ | 基本は通る。ただしオブジェクトリテラルを直接書いたときだけエラー(打ち間違い対策) |
| クラス | implements なしでも互換。ただし書いておくとクラス側でエラーになる |
private | あると形が変わり、同じ構造でも互換にならない |
| 意図的な区別 | ブランド型。実行時のコストはゼロ。使いどころは絞る |
次は 06. any・unknown・型アサーション です。型検査から降りる3つの方法と、その代償を扱います。実務で型が壊れる原因のほとんどがここです。