はじめに
この記事では、TypeScript
における応用的な型である、ユニオン型
・インターセクション型
・ジェネリック型
・リテラル型
・タプル型
についてまとめています。
応用的な型
TypeScriptでは、より柔軟で強力な型付けを実現するため
に応用的な型
が用意されています。
このような型を変数や式に持たせることで、型に矛盾が発生した場合にコンパイルエラーとなり、コーディングによるミスを未然に防げるという恩恵を得ることができます。
基本的な型(プリミティブ型 / ユーザ定義型)について は、こちらにまとめています。
ユニオン型とは
ユニオン型とは、型T または 型U
のような表現ができる型のことです。
複数の型のうち、いずれかの型
を表現するものです。
異なる型の値を受け入れる関数を設計する場合などに有用です。
メリット
ユニオン型は、複数の異なる型を、1つの変数や関数の引数で許可できます。
これにより、異なるシナリオでの変数の利用や関数の呼び出しにおいて、より幅広いデータ型をサポートすることができます。
使用例
2つ以上の型を パイプ記号 |
で繋げて、型TとUを用いてT | U
と書けます。
type Animal = {
species: string;
}
type Human = {
name: string;
}
// ユニオン型
type User = Animal | Human;
// ユニオン型
const mixed: string | number;
mixed = "hello"; // OK (string)
mixed = 42; // OK (number)
mixed = true; // エラー ('boolean' 型は 'string | number' 型に割り当てることができません。)
// ユニオン型は、以下のようにも書けます
type User =
| Animal
| Human;
配列要素 に ユニオン型 を使う際の書き方
type List = string | number[]; // 間違った記述
type List = (string | number)[]; // 正しい記述
判別可能なユニオン型
判別可能なユニオン型 とは、各型に固有のリテラル型のプロパティを持たせる
ことで、TypeScriptの型チェッカー がより効果的に動作するものです。
(判別可能なユニオン型は、タグ付きユニオン
や 直和型
と呼ぶこともあります。)
通常のユニオン型 は、絞り込みが複雑になりがち
ユニオン型 は自由度が高く、好きな型を組み合わせられますが、オブジェクトの型から成るユニオン型 を絞り込む際、分岐ロジックが複雑になりがちです。こういった場合に有効なのが、判別可能なユニオン型
です。
メリット
判別可能なユニオン型では、各型に 一意のリテラル型プロパティ(ディスクリミネータ
)を持たせることで、型を直接区別できるようになります。
これにより、型安全が向上し、コードをシンプルにすることができます。
使用例
以下の UploadStatus は、「ファイルアップロード状況」 を表現した ユニオン型 です。
- アップロード途中: InProgress
- アップロード成功: Success
- アップロード失敗: Failure
ここでは、固有のリテラル型のプロパティとしてtype
プロパティ を用意しています。
通常のユニオン型
type UploadStatus = InProgress | Success | Failure;
// ❌ 各型で、done の boolean を確認する必要がある。。
type InProgress = {
done: boolean; // 確認が必要
progress: number
};
type Success = {
done: boolean // 確認が必要
};
type Failure = {
done: boolean; // 確認が必要
error: Error
};
判別可能なユニオン型
type UploadStatus = InProgress | Success | Failure;
// ✅ 各型で、type により シンプルになる。
type InProgress = {
type: "InProgress"; // ディスクリミネータ
progress: number
};
type Success = {
type: "Success" // ディスクリミネータ
};
type Failure = {
type: "Failure"; // ディスクリミネータ
error: Error
};
// if文 と 型ガード を使用し、ディスクリミネータ を基にしたチェックにより、安全に型を区別できます。
function processUpload(status: UploadStatus) {
if (status.type === "InProgress") {
console.log(`進行中: ${status.progress}%`);
} else if (status.type === "Success") {
console.log("アップロード成功!");
} else if (status.type === "Failure") {
console.error(`エラー: ${status.error.message}`);
}
}
インターセクション型とは
インターセクション型とは、型T かつ 型U
のような表現ができる型です。
複数の型を1つに結合して、すべての特徴を持つ型
を表現するものです。
メリット
インターセクション型は、既存の型 を変更することなく新しい型を合成できます。
これにより、既存のコードの再利用性が高まり、機能の拡張が柔軟に行えます。
また、複数のインターフェイスや型を1つにまとめ包括的に持つ型を作成することで、大規模なアプリケーションやライブラリを設計する際に有用です。
使用例
2つ以上の型を 記号 &
で繋げて、型TとUを用いてT & U
と書けます。
type Animal = {
species: string;
}
type Human = {
name: string;
}
// インターセクション型
type User = Animal & Human;
type Name {
name: string;
}
type Age {
age: number;
}
// インターセクション型
type Person = Name & Age;
const john: Person = { name: "John", age: 30 }; // OK
また実際には、インターセクション型は、以下のように オブジェクト型を拡張した新しい型
を作成する際によく使われます。
type Animal = {
species: string;
age: number
}
type Human = Animal & {
name: string;
}
const mike: Animal = {
species: "persia",
age: 5
}
const john: Human = {
species: "neanderthal",
age: 30,
name: "john"
}
ジェネリック型とは
ジェネリック型とは、一言で表すと型を抽象化 (パラメーター化)
した型です。
また、ジェネリクス
機能を使用して作成された型 を指します。
「ジェネリック型」 or 「ジェネリクス型」 どっちが正しい?
- コミュニティや記事によっては、「ジェネリクス型」と記載されているものもありますが、公式には「ジェネリック型」が正式な様子。
よく見る、 型 T, U , K ,E とは?
-
T, U
は、型を表すためのパラメータ
として Generics で一般的によく使用されるものです。 - よく使われる 4つの名前の由来は以下です。
T: Type
U: Unknown
K: Key
E: Element
ジェネリクス (Generics) って?
型の安全性
と コードの共通化
を両立させるために導入された言語仕様のことです。
あらゆる型で、同じコードを使おうとすると、型の安全性
が犠牲になってしまいます。
型の安全性を重視すると、同じようなコードが量産され、コードの共通化
が難しくなります。
この問題を解決するために導入された機能が、ジェネリクスです。
メリット
ジェネリック型は、型をパラメーター化(抽象化)することができます。
これにより、型の安全性とコードの共通化を両立できるため、コンポーネントや関数が様々な型で動作できるようになり、汎用性を高くすることができます。
使用例
型名の後ろに<T(型引数)>
と囲って書けます。
以下の例では 型引数T
を持つ User型
を宣言しています。
User型 は、nameプロパティ が string型
であり、ageプロパティ が T型
である オブジェクトの型
となります。
type User<T> = {
name: string
age: T
}
const user1: User<number> = {
name: 'john',
age: 30
}
// 型引数T に string を渡しているため、age に number型 を代入することは出来ない
const user2: User<string> = {
name: 'john',
age: 30 // Type 'number' is not assignable to type 'string'.
}
以下のように、text / index
を それぞれ、string型 / number型
で受け取って、そのまま返す関数があったとします。このような似た処理を共通化
する際に ジェネリック型 が役立ちます。
// これは 下の showIndex と ほとんど一緒
const showText = (text: string): string => {
return text
}
// これは 上の showText と ほとんど一緒
const showIndex = (index: number): number => {
return index
}
// ↓ ジェネリック型 を利用して共通化
const genericFunction<T>(arg: T): T {
return arg;
}
genericFunction<string>("hoge") // showText() と同じ
genericFunction<number>(10) // showIndex() と同じ
リテラル型
リテラル型とは、特定の値のみ を その型として扱う型
です。
(プリミティブ型 を 細分化した型と言えます)
リテラル型には、以下の4種類があります。
どのリテラル型も使用法は同じで、例えば foo という文字列のリテラル型
は foo という値のみ許容
されます。
// 文字列 のリテラル型
const foo: "foo" = "foo";
// 数値 のリテラル型
const one: 1 = 1;
// 真偽値 のリテラル型
const t: true = true;
// BigInt のリテラル型
const second: 2n = 2n;
メリット
コンパイル時に意図しない値の使用
が検知できることで、コードの読みやすさや保守性を高められます。
使用例
// 文字列 のリテラル型
const foo: 'Hello';
foo = 'Bar'; // Error: "Bar" is not assignable to type "Hello"
// 真偽値 のリテラル型
const trueOrFalse: "true" | "false";
trueOrFalse = "true"; // OK
trueOrFalse = "maybe"; // エラー: '"maybe"' 型は '"true" | "false"' 型に割り当てることができません。
単独では使い所がほとんどありませんが、ユニオン型で結合して、便利な抽象化を作成することもできます。
type Direction =
| "North"
| "East"
| "South"
| "West";
function trip(distance: number, direction: Direction) {
// ...
}
trip(1,"North"); // Okay
trip(2,"East"); // Okay
trip(3,"South"); // Okay
trip(5,"West"); // Okay
trip(1,"Nurth"); // Error!
型注釈を用意したくない場合は、 as const を使用
上記のリテラル型のように、いちいち型注釈を用意したくない場合は、as const
(const assertion)を使うのが有効です。こちらを使用すると、いわゆる定数
として扱うことが可能になります。
この機能は、例) hoge as string
などとするような危険な機能ではなく、適切に使用すればTypeScriptプログラムの安全性を向上させてくれます。
使用例
// オブジェクトリテラル の例
const pikachu = {
name: "pikachu",
height: 0.4,
weight: 6.0,
} as const;
// as const によって、代入できないようになっています。 すべてのプロパティを read-only にしてくれています。
pikachu.name = "raichu"; // Cannot assign to 'name' because it is a read-only property.
// 配列 の例
const colors = ['red', 'green', 'blue'] as const;
// 要素の変更を試みる (エラー)
colors[0] = 'yellow'; // Error: Index signature in type 'readonly ["red", "green", "blue"]' only permits reading.
// 要素の追加を試みる (エラー)
colors.push('yellow'); // Error: Property 'push' does not exist on type 'readonly ["red", "green", "blue"]'.
// 完全に新しい配列を代入しようとする (エラー)
colors = ['yellow', 'pink']; // Error: Cannot assign to 'colors' because it is a constant.
タプル型
タプル型 とは、固定長の配列それぞれの要素に固有の型を持つことができる
型です。
配列のそれぞれの要素に、異なる型 を与えることができます。
メリット
通常の配列では同じ型の要素しか持てませんが、タプルを使うと数値、文字列、ブール値など異なる型の要素をひとつの配列で扱うことができます。これにより、複数の値を一緒に管理しなければならないが、それぞれの値が異なる型を持つ という場合に役立ちます。
使用例
// タプル型
let user: [string, number, boolean] = ["John", 30, true];
// タプル で 分割代入
const [name, age] = person;
タプル型の使い所としては、複数の非同期処理を並列に実行し、それぞれの結果 を 型安全に扱いたい場合などに有効です。(Promise.all と タプル型 の組み合わせ)
// Promise.all と タプル型 の組み合わせ
async function fetchUserData(): Promise<string> {
// 何らかのユーザーデータ を取得する非同期処理
return "user data";
}
async function fetchProductData(): Promise<number> {
// 何らかのプロダクトデータ を取得する非同期処理
return 123;
}
// 両方の非同期処理を並列で実行し、それぞれの結果 を タプル型 で受け取る
const result: [string, number] = await Promise.all([fetchUserData(), fetchProductData()]);
参考