TypeScriptのクロスタイプと高度な構成
交差型は & を使用して、複数の型を単一の型に結合します。この新しい型は、元の各型のすべての特性を備えています。これは、TypeScriptにおける複合型の中心的なツールです。
1. 交配タイプの基礎
(1) 基本的な構文
TYPESCRIPT
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
let person: Person = {
name: "Charlie",
age: 20
// Both must be present name and age
};
(2) ユニオン型との比較
| 演算子 | 意味 | 例え |
|---|---|---|
A & B (OR) |
A と B の両方を満たす | AND (and) |
| `A | B` (接続詞) | A または B を満たす |
TYPESCRIPT
type StringOrNumber = string | number; // Joint:It could be one of them
type StringAndNumber = string & number; // Crossing:It must be both at the same time → never!
// Meaningful Crossovers——Object Type
type HasId = { id: number };
type HasName = { name: string };
type Entity = HasId & HasName; // ✅ At the same time, there are id and name
(3) 複数のタイプの重複
TYPESCRIPT
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
type Auditable = { createdBy: string; updatedBy: string };
type FullEntity = Timestamped & SoftDeletable & Auditable;
let article: FullEntity = {
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
createdBy: "admin",
updatedBy: "editor"
};
2. 異なる型のデータのマージ規則
(1) 同じ名前を持つプロパティ――型の共通部分を抽出する
2つの型が、同じ名前のプロパティを持つが型が異なる場合、その共通部分の結果は、2つの型の共通部分となります:
TYPESCRIPT
type A = { value: string | number };
type B = { value: string | boolean };
type C = A & B;
// C 's value Type = (string | number) & (string | boolean) = string
let c: C = { value: "hello" }; // ✅ string It is the intersection
// let c2: C = { value: 42 }; // ❌ number Not in the intersection
// let c3: C = { value: true }; // ❌ boolean Not in the intersection
(2) 同じ名前のプロパティ — 互換性のない型の場合、「never」エラーが発生する
同じ名前を持つ 2 つの型のプロパティに重複がない場合、結果は never となります。このような値が存在することは不可能です:
TYPESCRIPT
type A = { id: string };
type B = { id: number };
type C = A & B;
// C 's id Type = string & number = never
// No value can be both string and number
// let c: C = { id: "x" }; // ❌ You cannot string Assigned never
⚠️ 注意: クロス型が
never となる場合、TypeScript は積極的にエラーをスローするわけではなく、単にその型を使用不可にするだけです。これが、クロス型とinterface extendsの主な違いです。extendsは競合が検出されるとエラーをスローしますが、&のクロス型は黙ってneverとなります。
(3) 関数型の交わり
関数型が共通する部分がある場合、パラメータはその共通部分として扱われます(通常、その結果は never となります):
TYPESCRIPT
type StringHandler = (value: string) => void;
type NumberHandler = (value: number) => void;
type MixedHandler = StringHandler & NumberHandler;
// MixedHandler parameters = string & number = never
// In fact, there is no value that can satisfy both signatures at the same time.
💡 ヒント: 通常の開発では、関数のオーバーロードはほとんど使用されません。「複数の型を扱える」関数が必要な場合は、オーバーロードではなく、ユニオン型を使用してください。
3. 十字型とinterface extendsの違い
(1) 構文の比較
TYPESCRIPT
// interface extends
interface Person {
name: string;
}
interface Employee extends Person {
employeeId: string;
}
// type Crossing
type Person2 = { name: string };
type Employee2 = Person2 & { employeeId: string };
(2) 紛争解決手法の比較
| シナリオ | インターフェースの拡張 | 型の交差 & |
|---|---|---|
| 同じ名前の属性型の互換性 | ✅ サブタイプがスーパタイプを上書き | 共通部分を取る |
| 同じ名前を持つ互換性のないプロパティ型 | ❌ コンパイルエラー | never が黙って生成される |
| 多重継承 | 単一継承のみ | 多重交差継承が許可される |
| 声明の統合 | 賛成 | 反対 |
(3) 選定に関する推奨事項
TYPESCRIPT
// Use extends scenario — Compile-time conflict detection is required
interface BaseConfig {
host: string;
port: number;
}
interface DevConfig extends BaseConfig {
debug: boolean; // ✅ New Properties
// host: number; // ❌ Compilation error——Parent Type host: string Conflict
}
// Using Intercut Scenes——Requires flexible combinations of multiple types
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithAudit = { createdBy: string; updatedBy: string };
type FullRecord = BaseConfig & WithTimestamps & WithAudit;
// Quick Combinations,No need to define an intermediate step interface
4. 型の組み合わせにおける一般的なパターン
(1) パターン 1:ミックスイン
インターフェース型を使用して、オブジェクトに追加の機能を「組み込む」方法:
TYPESCRIPT
type WithId = { id: number };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type WithSoftDelete = { deletedAt: Date | null };
type WithAudit = { createdBy: string; updatedBy: string };
// Freely combine different abilities
type BaseEntity = WithId & WithTimestamps;
type FullEntity = WithId & WithTimestamps & WithSoftDelete & WithAudit;
interface Article extends BaseEntity {
title: string;
content: string;
}
let article: Article = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
title: "TypeScript Getting Started",
content: "TypeScript is a superset of JavaScript..."
};
(2) モデル2:条件の組み合わせ
条件に基づいて特定のタイプをクロスさせるかどうかを選択してください:
TYPESCRIPT
type EntityWithOptional<T, TExtra> = T & Partial<TExtra>;
interface User {
id: number;
name: string;
}
interface UserProfile {
avatar: string;
bio: string;
}
// User + Optional Profile
type UserWithOptionalProfile = EntityWithOptional<User, UserProfile>;
// { id: number; name: string; avatar?: string; bio?: string }
(3) モデル3:ブランド別タイプ
クロスタイプを使用してプリミティブ型に「ラベル」を付け、その誤用を防ぐ:
TYPESCRIPT
type USD = number & { __brand: "USD" };
type EUR = number & { __brand: "EUR" };
function createUSD(amount: number): USD {
return amount as USD;
}
function createEUR(amount: number): EUR {
return amount as EUR;
}
let price: USD = createUSD(100);
let cost: EUR = createEUR(80);
// price = cost; // ❌ EUR Cannot be assigned to USD
// price + cost; // ❌ Cannot be combined
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD; // Calculations can be made within the same currency
}
let total = addUSD(price, createUSD(50));
console.log(total); // 150
▶ 例:型安全な設定の組み合わせ
TYPESCRIPT
// Basic Configuration
type BaseConfig = {
host: string;
port: number;
};
// Additional Development Environment Configuration
type DevConfig = BaseConfig & {
debug: true;
mockApi: boolean;
};
// Additional Configuration for the Production Environment
type ProdConfig = BaseConfig & {
debug: false;
ssl: boolean;
maxConnections: number;
};
function createDevConfig(): DevConfig {
return { host: "localhost", port: 3000, debug: true, mockApi: true };
}
function createProdConfig(): ProdConfig {
return { host: "api.example.com", port: 443, debug: false, ssl: true, maxConnections: 100 };
}
let dev = createDevConfig();
let prod = createProdConfig();
console.log(`Development:${dev.host}:${dev.port} (mock: ${dev.mockApi})`);
console.log(`Production:${prod.host}:${prod.port} (ssl: ${prod.ssl})`);
出力:
TEXT
Development:localhost:3000 (mock: true)
Production:api.example.com:443 (ssl: true)
5. クロスタイプ操作の高度なテクニック
(1) 再帰的な型変換
TYPESCRIPT
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Array<any>
? T[K]
: DeepPartial<T[K]>
: T[K];
};
interface Config {
server: { host: string; port: number };
database: { url: string; pool: { min: number; max: number } };
}
type PartialConfig = DeepPartial<Config>;
let cfg: PartialConfig = {}; // All levels are available
(2) 汎用関数における交点
TYPESCRIPT
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
let person = merge({ name: "Charlie" }, { age: 20 });
// person Type:{ name: string } & { age: number }
(3) 条件付き型における交点
TYPESCRIPT
type AddTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
interface User {
name: string;
email: string;
}
type TimestampedUser = AddTimestamps<User>;
// { name: string; email: string; createdAt: Date; updatedAt: Date }
❓ よくある質問
Q 型の交差によって発生する「never」エラーのトラブルシューティング方法は?
A 交差しているメンバーを順次削除し、どの2つの型が競合しているかを確認してください。よくある原因として、同じ名前を持つ互換性のないプロパティ型(例:
string & number)が挙げられます。交差の代わりに interface extends を使用することをお勧めします。交差の場合、extends は競合が発生すると即座にエラーをスローするため、問題の特定が容易になります。Q クロスタイピングは継承に取って代わることができますか?
A ほとんどの場合可能です。ただし、両者は完全に同等というわけではありません。
interface extendsは、コンパイル時の競合検出、宣言の統合、およびclass implementsのサポートを提供します。クロスタイピングはより柔軟ですが、競合検出機能がありません。オブジェクト型の組み合わせにはextendsを、単純で迅速な組み合わせにはクロスタイピングを使用することをお勧めします。Q ブランド型は実際の開発で役に立ちますか?
A 構造は同じだが意味が異なる型を区別する必要がある場合、非常に役立ちます。代表的な例としては、通貨(USD 対 EUR)、ID(UserId 対 OrderId)、測定単位(メートル 対 フィート)などが挙げられます。ブランド付き型は、意図しない混同を防ぎ、ユーロをドルとして扱うといったエラーをコンパイル時に検出します。
Q 選択型と結合型を併用することはできますか?
A はい。
type T = (A & B) | (C & D) は「A と B の両方が満たされるか、C と D の両方が満たされるか」を意味します。括弧によって優先順位が決まります。つまり、& は | より優先順位が高いため、A & B | C & D は (A & B) | (C & D) と同等となります。📖 まとめ
- クロスタイプ
A & B:複数のタイプのすべての特徴を組み合わせたもので、この新しいタイプは構成要素となるすべてのタイプの要件を同時に満たさなければならない - 同じ名前のプロパティ型が交差する場合、互換性のない型があると
neverが発生します。TypeScript はこれを積極的にエラーとして報告しません。 interface extendsには競合検出機能が含まれていますが、typeはより柔軟性が高くなっています。状況に応じて選択してください。- 一般的な組み合わせパターン:ミックスイン、条件付き組み合わせ、ブランド固有の型
- ブランドタイプには
& { __brand: "label" }を使用し、構造は同じだが意味が異なるタイプが混在するのを防ぐ
📝 練習問題
- 基本問題(難易度 ⭐):2つの型
WithIdとWithTimestampsを定義し、クロス型を用いてこれらを組み合わせ、BaseEntityを形成し、BaseEntityを満たすオブジェクトを作成する。 - 上級問題(難易度 ⭐⭐):ブランドタイプ
UserId = number & { __brand: "UserId" }およびOrderId = number & { __brand: "OrderId" }を実装してください。それぞれのタイプの ID を作成する関数を記述し、それらが互いに割り当てられないことを確認してください。 - チャレンジ問題(難易度 ⭐⭐⭐):
Overwrite<T, U>ツール型を実装してください。T 内の同名のプロパティを U のプロパティで上書きしつつ、異なる名前のプロパティは保持するようにしてください。ヒント:マッピング型とクロスタイプを使用してください。



