LoginSignup
2
1

【TypeScript】応用的な型 を使った定義

Last updated at Posted at 2024-05-19

はじめに

この記事では、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()]);

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1