本文へスキップ
スキルアップカレッジ

型推論とユニオン型——書かなくても型はついている

レッスン3:型推論とユニオン型——書かなくても型はついている

このレッスンで学ぶこと

  • 型推論の仕組みを理解し、型注釈を省略できる場面を判断できる
  • let と const で推論される型がどう変わるかを説明できる
  • ユニオン型とリテラル型で、複数の可能性を型に表現できる
  • null と undefined を安全に扱い、型の絞り込みができる

レッスン2 では、文字列・数値・配列・オブジェクトへの型注釈の書き方と、any の危険性を学びました。レッスンの終わりでは、「値を代入すると同時に宣言する変数は、型注釈を省略できる場合がある」という話に軽く触れました。このレッスンでは、その仕組みである「型推論」を中心に据えて、TypeScript らしい書き方を身につけます。

型推論とは——書かなくても型は決まっている

TypeScript は、変数に代入された値を見て、型を自動的に判断します。この仕組みを型推論と呼びます。

const userName = "森田 彩花";
const userAge = 30;
const isInstructor = true;

このコードには型注釈が 1 つも書かれていませんが、TypeScript の内部では、userName は string、userAge は number、isInstructor は boolean という型がすでに決まっています。試しに、userName へ数値を代入するコードを追加してコンパイルしてみましょう。

const userName = "森田 彩花";
userName = 30;
error TS2322: Type 'number' is not assignable to type 'string'.

型注釈を書いていないにもかかわらず、レッスン2 で string という型注釈を書いたときとまったく同じエラーが出ます。TypeScript が「userName には最初に文字列が入ったのだから、この変数は文字列専用だろう」と判断し、以降もその型を守らせているためです。

💡 ポイント 型推論は「型注釈を省略できる便利機能」ではなく、「TypeScript が常に型を把握し続けている」という前提そのものです。型注釈を書いていない変数にも、水面下では必ず型が付いています。中核メッセージの 1 つである「型は全部書かなくてよい。推論に任せられるところは任せる」は、この仕組みの上に成り立っています。

配列やオブジェクトでも、同じように推論が働きます。

const scores = [85, 62, 78];
const user = { name: "森田 彩花", age: 30 };

scores は number[]、user は { name: string; age: number } として、レッスン2 で手で書いた型注釈とまったく同じ型が自動的に付きます。

コンテキストからの型推論——配列のメソッドでも型は付いている

型推論は、変数への代入だけでなく、配列のメソッドに渡すコールバック関数の引数でも働きます。「JavaScript 入門」で学んだ forEach や map を、型注釈のある配列に対して使ってみましょう。

const scores: number[] = [85, 62, 78];
scores.forEach((score) => {
  console.log(score.toFixed(1));
});

const fruits: string[] = ["りんご", "みかん"];
const upperFruits = fruits.map((fruit) => fruit.toUpperCase());

score や fruit にはどこにも型注釈を書いていませんが、TypeScript は scores が number[]、fruits が string[] であることから、「コールバックに渡される各要素はこの型のはずだ」と判断し、それぞれを number・string として扱います。これを「コンテキストからの型推論」と呼びます。「今どこで使われているか」という文脈(コンテキスト)から、型を逆算して決めているイメージです。配列の要素にすでに型が付いている以上、その配列を処理するコールバック関数にまで、いちいち型注釈を書き直す必要はありません。これも「推論に任せられるところは任せる」という考え方の実例です。

letとconstで推論のされ方が変わる

型推論には、let で宣言するか const で宣言するかによって、少し違う結果になるという特徴があります。

const status = "active";
let status2 = "active";

const status の型は、string という広い型ではなく "active" という、その文字列そのものを表す型に推論されます。これを「リテラル型」と呼びます。const で宣言した status には、この後どんな値も再代入できないため、TypeScript は「値が変わらないなら、値そのものを型にしてしまってよい」と判断するのです。

一方、let status2 は、後から値が変わる可能性があるため、"active" という狭い型ではなく string という広い型に推論されます。これを「型が広がる(widening)」と呼びます。

let status2 = "active";
status2 = "completed"; // let なので型が string に広がっており、別の文字列も代入できる

const status = "active";
status = "completed"; // エラー:const なので再代入自体ができない

let と const の使い分け自体は前提知識として扱いますが、「const は値そのものが型になる」という推論結果の違いは、この後に学ぶリテラル型を理解するうえで重要な土台になります。

ユニオン型——複数の可能性を型で表す

ここまでの型は「1 つの値は 1 つの型」という前提で進めてきました。しかし実際のコードでは、「文字列か数値のどちらかが入る」というように、複数の可能性を持つ値を扱う場面があります。これを表すのがユニオン型です。縦棒 | で型をつなげて書きます。

let userId: string | number;

userId = "u-001";
userId = 12345;

string | number は「文字列、または数値」という意味です。どちらの型の値も代入できますが、それ以外の型(真偽値や配列など)を代入しようとすると、コンパイルの段階で止められます。

ユニオン型は、関数の引数でもよく使われます。

function formatId(id: string | number) {
  return `ID: ${id}`;
}

console.log(formatId("u-001"));
console.log(formatId(12345));

会員 ID を文字列で管理しているシステムと、数値で管理しているシステムが混在するときなど、「どちらの形式で来てもよい」ことを表すのにユニオン型が向いています。

ユニオン型は 2 つの型に限らず、3 つ以上つなげることもできます。

function logValue(value: string | number | boolean) {
  console.log(`受け取った値:${value}`);
}

logValue("完了");
logValue(100);
logValue(true);

string | number | boolean のように、必要な分だけ型を | でつなげられます。ただし、ユニオン型を広げすぎると「結局どんな値でも受け付ける」状態に近づき、any に近い緩さになってしまいます。本当に必要な型だけに絞り込んで書くことが、ユニオン型を活かすコツです。

📝 補足 ユニオン型の値は、ユニオンに含まれるすべての型に共通する操作しかできません。string | number の値に対して、いきなり文字列専用の toUpperCase を呼び出そうとすると、「number にはその操作がない」という趣旨のエラーになります。共通しない操作をしたいときは、次に説明する型の絞り込みが必要です。

リテラル型——値そのものを型にする

先ほど const の推論結果として登場したリテラル型は、単独でも型注釈として明示的に書けます。

let size: "small" | "medium" | "large";

size = "medium";
size = "extra-large"; // エラー:3つの候補に含まれない

"small" | "medium" | "large" は、「この 3 つの文字列のどれか」だけを許すユニオン型です。文字列型全体(string)を許すのではなく、あらかじめ決まった候補だけに絞り込めるのが、リテラル型とユニオン型を組み合わせたときの強みです。

function setSize(size: "small" | "medium" | "large") {
  console.log(`サイズを${size}に設定しました`);
}

setSize("medium");
setSize("huge"); // エラー:候補にない値

商品のサイズ、注文の状態(「注文済み」「発送済み」「完了」など)、ボタンの種類のように、あらかじめ決まった選択肢しか許したくない値を扱うとき、string 型よりもリテラル型のユニオンのほうが、間違った文字列を渡す事故を防げます。

nullとundefinedを安全に扱う

「JavaScript 入門」で学んだとおり、JavaScript には null(明示的な空)と undefined(まだ値が設定されていない状態)という 2 つの特別な値があります。TypeScript では、この 2 つも独立した型として扱われます。

let selectedItem: string | null;

selectedItem = "りんご";
selectedItem = null;

string | null は「文字列、またはまだ何も選ばれていない状態」を表すユニオン型です。何も選ばれていない状態を null という値で明示的に表しておくことで、「まだ選ばれていない」というケースを型の上でも扱えるようになります。

TypeScript には、厳格な設定にすると、string 型の変数に null や undefined を代入できなくなる仕組みがあります。レッスン1 で npx tsc --init を実行したときに作られる tsconfig.json は、この厳格な設定が有効になった状態が初期状態です。そのため、null が入る可能性のある値には、最初から string | null のようにユニオン型で明示しておくのが安全な書き方になります。

⚠️ 注意 厳格な設定が有効な環境では、string | null 型の値に対して、null の可能性を無視した操作をするとコンパイルエラーになります。次に説明する「型の絞り込み」を使って、安全に中身を取り出す必要があります。

null とよく似た値に undefined があります。「JavaScript 入門」で確認したとおり、null は「明示的な空」、undefined は「まだ値が設定されていない状態」という違いがあります。TypeScript では、この違いをそのまま型のレベルでも扱えます。

function findUser(id: string): { name: string } | undefined {
  const users = [{ id: "u-1", name: "森田 彩花" }];
  return users.find((user) => user.id === id);
}

const result = findUser("u-2");

find は、条件に合う要素が見つからなければ何も返しません。この「見つからなかった」状態を表すのに undefined を使い、戻り値の型を { name: string } | undefined としています。呼び出し側は、result が見つかったオブジェクトなのか、見つからなかった undefined なのかを、この型を見るだけで判断できます。

レッスン2 で学んだオプショナルなプロパティ(email?: string)も、内部的には string | undefined と同じ意味を持っています。プロパティの末尾に ? を付けることと、型に | undefined を加えることは、実質的に同じことを表す 2 つの書き方だと理解しておくと、この先の混乱を防げます。

型の絞り込み——ユニオン型を安全に使う

ユニオン型の値をそのまま使おうとすると、TypeScript は「どちらの型か確定していない」として操作を止めることがあります。

function printLength(value: string | null) {
  console.log(value.length); // エラー:null の可能性がある
}

これを安全に使うには、条件分岐で「今はどちらの型なのか」を確認します。この作業を型の絞り込み(narrowing)と呼びます。

function printLength(value: string | null) {
  if (value !== null) {
    console.log(value.length); // ここでは value は string だと確定している
  } else {
    console.log("値がありません");
  }
}

if (value !== null) の内側では、TypeScript は「value は null ではない、つまり string のはずだ」と判断してくれます。これが型の絞り込みです。if 文で条件分岐をすること自体は前提知識ですが、その条件分岐が型そのものを狭めてくれる点が、TypeScript ならではの働きです。

typeof を使った絞り込みも、レッスン2 で扱った unknown 型と組み合わせてよく使われます。

function describe(value: string | number) {
  if (typeof value === "string") {
    console.log(`文字列です:${value.toUpperCase()}`);
  } else {
    console.log(`数値です:${value.toFixed(2)}`);
  }
}

typeof value === "string" で分岐した内側では value が string として扱われ、toUpperCase のような文字列専用のメソッドを安心して呼び出せます。typeof そのものの使い方は前提知識のとおりですが、それを型の絞り込みに使う発想が、この先ずっと役立ちます。

配列かどうかを確認したいときは、Array.isArray を使った絞り込みも定番です。

function printAll(value: string | string[]) {
  if (Array.isArray(value)) {
    value.forEach((item) => console.log(item));
  } else {
    console.log(value);
  }
}

Array.isArray(value) が true の場合、TypeScript は value を string[] として扱ってくれるため、forEach をそのまま呼び出せます。false の場合は自動的に残りの型である string に絞り込まれます。「条件式 1 つで、両方の枝それぞれの型が確定する」という感覚は、慣れるまでは不思議に感じるかもしれませんが、ユニオン型を扱う場面では頻繁に登場する考え方です。

型の絞り込みの流れを図にすると、次のようになります。

flowchart LR
    A["ユニオン型の値<br/>string または null"] --> B{"value !== null で確認"}
    B -->|true| C["string として扱える"]
    B -->|false| D["null 用の処理をする"]

この図は、幅の広いユニオン型が、条件分岐を通ることで徐々に絞り込まれていく流れを表しています。分岐に入る前は「どちらか確定していない」状態ですが、条件を確認した後は、それぞれの枝の中で型が確定します。

実習:注文ステータスを扱う関数を作る

ここまでの内容を組み合わせて、注文の状態を表示する関数を作ってみましょう。

type OrderStatus = "pending" | "shipped" | "completed";

function describeOrder(id: string | number, status: OrderStatus, note: string | null) {
  const label =
    status === "pending" ? "受付中" :
    status === "shipped" ? "発送済み" : "完了";

  console.log(`注文ID:${id}`);
  console.log(`状態:${label}`);

  if (note !== null) {
    console.log(`備考:${note}`);
  } else {
    console.log("備考:なし");
  }
}

describeOrder("o-1001", "shipped", "配達時間の指定あり");
describeOrder(1002, "pending", null);

このコードでは、ID にユニオン型(string | number)、状態にリテラル型のユニオン(OrderStatus)、備考に null を含むユニオン型を使っています。type OrderStatus = ... という書き方は、リテラル型のユニオンに名前を付けて使い回すための構文で、レッスン5 で type エイリアスとして詳しく扱います。ここでは「複数の可能性を型で表す」という考え方が、実際のコードでどう組み合わさるかを体験してください。

まとめ

このレッスンでは、以下のことを学びました。

  • TypeScript は代入された値から型を自動的に判断する(型推論)。型注釈がなくても型は常に決まっている
  • const は値そのものを表すリテラル型に、let は広い型に推論される
  • ユニオン型(型A | 型B)で、複数の可能性を持つ値を型で表現できる
  • リテラル型とユニオン型を組み合わせると、決まった候補だけを許す型を作れる
  • null や undefined はユニオン型に含めて明示し、条件分岐で型を絞り込んでから使う

次のレッスンでは、関数そのものに型を付ける方法を学びます。引数と戻り値への型注釈、省略可能な引数、そして値を返さない関数の型を扱い、既刊のコースで書いた関数を型付きで書き直していきます。


確認クイズ

このレッスンの理解度をチェックしましょう。