TypeScript入門
型安全なJavaScript開発を始める
TypeScriptの基本的な型システムを基礎から解説。型定義、インターフェース、ジェネリクス、型ガードまで、型安全な開発の基礎を学べます。
こんな人向けの記事です
- TypeScriptを初めて学ぶ方
- JavaScriptの型の問題を解決したい方
- インターフェースやジェネリクスを理解したい方
Step 1TypeScriptとは
TypeScriptは、Microsoftが開発したJavaScriptに静的型付けを追加したプログラミング言語です。TypeScriptのコードはコンパイル時にJavaScriptに変換されるため、JavaScriptが動作するすべての環境で利用できます。
TypeScriptは「JavaScriptのスーパーセット」です。すべてのJavaScriptコードはそのままTypeScriptとしても有効です。既存のJSプロジェクトに段階的に導入できるのが大きなメリットです。
JavaScriptとTypeScriptの主な違いを比較してみましょう。
| 特徴 | JavaScript | TypeScript |
|---|---|---|
| 型システム | 動的型付け | 静的型付け(コンパイル時チェック) |
| エラー検出 | 実行時 | コンパイル時(開発中に発見) |
| IDE補完 | 限定的 | 強力な自動補完・リファクタリング |
| 実行環境 | ブラウザ・Node.jsで直接実行 | コンパイル後にJSとして実行 |
| 学習コスト | 低い | 型の知識が必要(段階的に学習可能) |
JavaScriptで起きやすい型エラーをTypeScriptがどう防ぐか見てみましょう。
// JavaScriptでは実行するまでエラーに気づけない
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet(42); // 実行時エラー: name.toUpperCase is not a function
// TypeScriptではコンパイル時にエラーを検出
function greet(name: string): string {
return "Hello, " + name.toUpperCase();
}
greet(42); // コンパイルエラー: Argument of type 'number' is not assignable to parameter of type 'string'
TypeScriptの開発環境をセットアップするには、Node.jsがインストールされている環境で以下のコマンドを実行します。
# TypeScriptをインストール
npm install -g typescript
# バージョン確認
tsc --version
# プロジェクト初期化(tsconfig.jsonを生成)
tsc --init
# TypeScriptファイルをコンパイル
tsc hello.ts
Step 2基本的な型
TypeScriptで最もよく使う基本型を見ていきましょう。変数宣言時に: 型名を付けることで型を指定できます(型アノテーション)。
プリミティブ型
// string: 文字列
let userName: string = "太郎";
// number: 数値(整数・小数の区別なし)
let age: number = 25;
let price: number = 1980.5;
// boolean: 真偽値
let isActive: boolean = true;
// null と undefined
let nothing: null = null;
let notDefined: undefined = undefined;
// any: 型チェックを無効化(使用は最小限に)
let flexible: any = "hello";
flexible = 42; // エラーにならない
flexible = true; // エラーにならない
any型は型チェックを完全に無効化するため、TypeScriptの恩恵を受けられなくなります。やむを得ない場合を除き、具体的な型を指定するか、後述のunknown型を使いましょう。
配列(Array)
// 配列の型指定(2つの書き方)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob", "Charlie"];
// 型が異なる要素を追加するとエラー
numbers.push("six"); // コンパイルエラー: Argument of type 'string' is not assignable
// 読み取り専用配列
let readonlyArr: readonly number[] = [1, 2, 3];
readonlyArr.push(4); // コンパイルエラー: Property 'push' does not exist
タプル(Tuple)
// タプル: 要素数と各要素の型が固定された配列
let user: [string, number] = ["太郎", 25];
// 各要素の型が保証される
let name: string = user[0]; // OK: string
let age: number = user[1]; // OK: number
// 型が合わないとエラー
let wrong: [string, number] = [25, "太郎"]; // コンパイルエラー
// ラベル付きタプル(可読性向上)
type UserTuple = [name: string, age: number, isAdmin: boolean];
let admin: UserTuple = ["管理者", 30, true];
列挙型(Enum)
// 数値enum(デフォルトは0から連番)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
let move: Direction = Direction.Up;
console.log(move); // 0
// 文字列enum(値を明示的に指定)
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
let currentStatus: Status = Status.Active;
console.log(currentStatus); // "ACTIVE"
文字列enumはデバッグ時に値が読みやすく、意図しない比較を防げるため、数値enumより推奨されます。ただし、後述のユニオン型で代替できるケースも多いです。
基本型の全体像をまとめます。
| 型 | 説明 | 例 |
|---|---|---|
string |
文字列 | "hello" |
number |
数値(整数・小数) | 42, 3.14 |
boolean |
真偽値 | true, false |
Array<T> |
配列 | [1, 2, 3] |
[T, U] |
タプル | ["name", 25] |
enum |
列挙型 | Direction.Up |
any |
型チェック無効 | なるべく使わない |
unknown |
安全なany | 型ガードと組み合わせる |
void |
戻り値なし | 関数の戻り値 |
never |
到達しない型 | 例外を投げる関数 |
Step 3インターフェースと型エイリアス
オブジェクトの構造(どんなプロパティを持つか)を定義する方法として、インターフェースと型エイリアスの2つがあります。
インターフェース(interface)
// インターフェースでオブジェクトの形を定義
interface User {
id: number;
name: string;
email: string;
age?: number; // ?を付けるとオプショナル(省略可能)
readonly createdAt: Date; // readonlyで変更不可
}
// インターフェースに従ったオブジェクトを作成
const user: User = {
id: 1,
name: "太郎",
email: "taro@example.com",
createdAt: new Date(),
// age は省略可能
};
user.createdAt = new Date(); // コンパイルエラー: readonlyプロパティは変更不可
// 必須プロパティが欠けるとエラー
const badUser: User = {
id: 2,
name: "花子",
// email が無い → コンパイルエラー
};
インターフェースの拡張(extends)
// 基本インターフェース
interface Animal {
name: string;
age: number;
}
// 拡張(継承)
interface Dog extends Animal {
breed: string;
bark(): void;
}
// Dog は Animal のプロパティも持つ
const dog: Dog = {
name: "ポチ",
age: 3,
breed: "柴犬",
bark() {
console.log("ワン!");
},
};
// 複数のインターフェースを拡張
interface Pet extends Animal {
owner: string;
}
interface PetDog extends Dog, Pet {
trained: boolean;
}
型エイリアス(type)
// 型エイリアスでオブジェクトの形を定義
type Product = {
id: number;
name: string;
price: number;
category: string;
};
// プリミティブ型にも別名を付けられる(interfaceではできない)
type ID = number | string;
type Callback = (data: string) => void;
// 交差型(&)で型を合成
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type TimestampedProduct = Product & Timestamped;
const product: TimestampedProduct = {
id: 1,
name: "ノートPC",
price: 98000,
category: "電子機器",
createdAt: new Date(),
updatedAt: new Date(),
};
インターフェースと型エイリアスの使い分けをまとめます。
| 機能 | interface | type |
|---|---|---|
| オブジェクト型の定義 | 可能 | 可能 |
| 拡張(extends) | 可能 | 交差型(&)で代替 |
| 宣言マージ | 可能(同名で自動統合) | 不可 |
| プリミティブ型の別名 | 不可 | 可能 |
| ユニオン型の定義 | 不可 | 可能 |
| 推奨シーン | オブジェクト・クラスの形状定義 | ユニオン型・複合型・関数型 |
迷ったらinterfaceを使い、ユニオン型や関数型などinterfaceで表現できないものにtypeを使うのが一般的なベストプラクティスです。
Step 4ユニオン型とリテラル型
ユニオン型は「AまたはB」のように、複数の型のいずれかを取れる値を表現します。リテラル型と組み合わせることで、取りうる値を厳密に制限できます。
ユニオン型
// ユニオン型: | で複数の型を許容
let id: string | number;
id = "abc-123"; // OK
id = 42; // OK
id = true; // コンパイルエラー: boolean は許容されない
// 関数の引数にユニオン型を使用
function formatId(id: string | number): string {
// 共通のメソッドしか使えない(型を絞り込むまで)
return String(id).padStart(5, "0");
}
console.log(formatId("123")); // "00123"
console.log(formatId(42)); // "00042"
// 配列のユニオン型
let mixed: (string | number)[] = [1, "two", 3, "four"];
リテラル型
// リテラル型: 特定の値のみ許容する型
type Direction = "north" | "south" | "east" | "west";
let heading: Direction = "north"; // OK
heading = "northeast"; // コンパイルエラー
// 数値リテラル型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3; // OK
roll = 7; // コンパイルエラー
// 実用例: HTTPメソッド
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
function fetchData(url: string, method: HttpMethod): void {
console.log(url, method);
}
fetchData("/api/users", "GET"); // OK
fetchData("/api/users", "REMOVE"); // コンパイルエラー
判別可能なユニオン型(Discriminated Union)
// 共通のプロパティ(kind)で型を判別する
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
// ここでは shape は Circle 型に絞り込まれる
return Math.PI * shape.radius ** 2;
case "rectangle":
// ここでは shape は Rectangle 型に絞り込まれる
return shape.width * shape.height;
case "triangle":
// ここでは shape は Triangle 型に絞り込まれる
return (shape.base * shape.height) / 2;
}
}
console.log(calculateArea({ kind: "circle", radius: 5 })); // 78.54
console.log(calculateArea({ kind: "rectangle", width: 4, height: 6 })); // 24
判別可能なユニオン型はTypeScriptで頻出するパターンです。Reduxのアクション定義、APIレスポンスの処理、状態管理など、多くの場面で活用されます。
Step 5ジェネリクス
ジェネリクスは「型をパラメータとして受け取る」仕組みです。関数やクラスを特定の型に依存させず、再利用可能にしながら型安全性を保てます。
ジェネリック関数
// anyを使うと型情報が失われる(悪い例)
function firstElementBad(arr: any[]): any {
return arr[0];
}
const result1 = firstElementBad([1, 2, 3]); // 型は any
// ジェネリクスを使うと型が保持される(良い例)
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // 型は number | undefined
const str = firstElement(["a", "b", "c"]); // 型は string | undefined
// 複数の型パラメータ
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // 型は [string, number]
ジェネリック制約(extends)
// T に制約を設ける: length プロパティを持つ型のみ許容
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(value.length);
}
logLength("hello"); // OK: string は length を持つ
logLength([1, 2, 3]); // OK: 配列は length を持つ
logLength(42); // コンパイルエラー: number は length を持たない
// keyof を使った制約
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "太郎", age: 25, email: "taro@example.com" };
const name = getProperty(user, "name"); // 型は string
const age = getProperty(user, "age"); // 型は number
getProperty(user, "phone"); // コンパイルエラー: "phone" は User のキーではない
ジェネリックインターフェース
// APIレスポンスの共通型
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}
// 具体的な型を指定して使用
interface User {
id: number;
name: string;
}
interface Product {
id: number;
name: string;
price: number;
}
// 同じ ApiResponse 型を異なるデータ型で再利用
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "太郎" },
status: 200,
message: "Success",
timestamp: new Date(),
};
const productResponse: ApiResponse<Product[]> = {
data: [
{ id: 1, name: "ノートPC", price: 98000 },
{ id: 2, name: "マウス", price: 3500 },
],
status: 200,
message: "Success",
timestamp: new Date(),
};
ジェネリクスは「中身は何でもいいが、一貫性は保ちたい」ときに使います。ApiResponse<T>のように、レスポンスの構造は同じだがデータの型だけ変わる場面で威力を発揮します。
Step 6型ガードとナローイング
ユニオン型の変数に対して、条件分岐で型を絞り込む(ナローイングする)技法を型ガードと呼びます。TypeScriptコンパイラは条件分岐を解析して、各ブロック内で型を自動的に絞り込みます。
typeof による型ガード
function processValue(value: string | number | boolean): string {
// typeof で型を絞り込む
if (typeof value === "string") {
// ここでは value は string 型
return value.toUpperCase();
} else if (typeof value === "number") {
// ここでは value は number 型
return value.toFixed(2);
} else {
// ここでは value は boolean 型
return value ? "YES" : "NO";
}
}
console.log(processValue("hello")); // "HELLO"
console.log(processValue(3.14)); // "3.14"
console.log(processValue(true)); // "YES"
instanceof による型ガード
class Dog {
bark(): string {
return "ワンワン!";
}
}
class Cat {
meow(): string {
return "ニャー!";
}
}
function makeSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
// ここでは animal は Dog 型
return animal.bark();
} else {
// ここでは animal は Cat 型
return animal.meow();
}
}
console.log(makeSound(new Dog())); // "ワンワン!"
console.log(makeSound(new Cat())); // "ニャー!"
in 演算子による型ガード
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
// ここでは animal は Fish 型
animal.swim();
} else {
// ここでは animal は Bird 型
animal.fly();
}
}
ユーザー定義型ガード
interface Admin {
role: "admin";
permissions: string[];
}
interface Guest {
role: "guest";
visitCount: number;
}
type User = Admin | Guest;
// 戻り値の型「user is Admin」が型ガードの宣言
function isAdmin(user: User): user is Admin {
return user.role === "admin";
}
function showDashboard(user: User): void {
if (isAdmin(user)) {
// ここでは user は Admin 型
console.log("権限:", user.permissions.join(", "));
} else {
// ここでは user は Guest 型
console.log("訪問回数:", user.visitCount);
}
}
showDashboard({ role: "admin", permissions: ["read", "write", "delete"] });
// 出力: 権限: read, write, delete
showDashboard({ role: "guest", visitCount: 5 });
// 出力: 訪問回数: 5
ユーザー定義型ガードの判定ロジックが間違っていると、コンパイラは信用してしまうため実行時エラーの原因になります。型ガード関数のテストは必ず書きましょう。
Exhaustiveness Check(網羅性チェック)
type Status = "active" | "inactive" | "pending";
function getStatusLabel(status: Status): string {
switch (status) {
case "active":
return "有効";
case "inactive":
return "無効";
case "pending":
return "保留中";
default:
// never型に代入: すべてのケースを処理していることを保証
const _exhaustive: never = status;
return _exhaustive;
}
}
// 将来 Status に "archived" を追加した場合、
// switch文で処理しないと default でコンパイルエラーになる
網羅性チェックはnever型を使ったテクニックです。ユニオン型に新しいメンバーを追加したとき、処理を書き忘れた箇所をコンパイラが教えてくれるため、バグの防止に非常に効果的です。
TypeScript入門チェックリスト
- TypeScriptはJavaScriptのスーパーセットで、静的型付けを追加する
- 基本型: string, number, boolean, Array, Tuple, Enum
- interfaceはオブジェクトの形を定義し、extendsで拡張できる
- typeはユニオン型・交差型・関数型など柔軟な型定義に使う
- ユニオン型(A | B)で複数の型を許容、リテラル型で値を制限
- ジェネリクスで型をパラメータ化し、再利用性と型安全性を両立
- 型ガード(typeof, instanceof, in, ユーザー定義)で型を絞り込む
- never型で網羅性チェックを行い、処理漏れを防ぐ