TypeScript入門①
Web アプリケーション開発において、TypeScript は今や欠かせないツールとなりつつあります。JavaScript に「型」という概念を導入することで、コードの品質と保守性を飛躍的に向上させることができます。
この記事では、JavaScript の知識がある方を対象に、TypeScript の主要な機能を厳選して解説します。TypeScript の基本的な型システムから、日々の開発で役立つ具体的な機能まで、コード例を交えながら分かりやすくご紹介していきます。
型システムと TypeScript の検査
TypeScript では「型システム」という仕組みを使用して、コードをコンパイル前に検査します。
型システム
値に型を与えて、その型に合った使い方だけを許す仕組みです。
型があることで安全で間違いの少ないプログラムが書けます。
型注釈 - 明示的に型を書く
変数に「どんな型の値が入るか」を明示することで、間違った型を入れたときにエラーが出ます。
const name: string = 'Taro';
// └ :string が型注釈
型推論 - 値から自動で型を判断
値から型を自動で推測してくれる場合があります。
const age = 20; // 🤖 自動で number 型と判断されます
基本的な型
プリミティブ型
TypeScript では、変数や関数の「値の種類(型)」を指定できます。
これを型注釈といい、JavaScript の基本的な型(string や number など)をそのまま使えます。
型 | 説明 | 使用例 |
---|---|---|
boolean |
真偽値(true / false ) |
const isDone: boolean = true; |
number |
数値(整数・小数のどちらも) | const age: number = 18; |
string |
文字列 | const name: string = "Taro"; |
bigint |
非常に大きな整数(末尾に n ) |
const big: bigint = 123456789n; |
symbol |
一意な識別子を作る特殊な型 | const sym: symbol = Symbol("id"); |
undefined |
値が定義されていない状態 | const x: undefined = undefined; |
null |
値が存在しない(明示的に「ない」) | const y: null = null; |
特殊な型
特定の目的のために使う TypeScript 独自の型です。
使い方によっては型安全を損なったり、逆に守ったりするので、状況に応じて使い方をしっかり考える必要があります。
型 | 特徴 | 安全性 | 使用例 |
---|---|---|---|
any |
何でも代入 OK。型チェックなし | ❌ 低い | サンプルコード、プロトタイプ |
unknown |
何でも代入 OK。ただし操作前に型チェック必須 | ✅ 高い | ライブラリの引数など |
void |
関数が値を返さない | ✅ 高い | ログ出力、通知関数など |
never |
絶対に戻ってこない | ✅ 高い | エラー処理、無限ループ |
any
型 – どのような値も代入できる型チェックをしない型
any
型は、どのような値でも代入できる特別な型できますが、どのような値でも代入できる特別な型ですが、変数やプロパティへのアクセス、メソッドの呼び出しなどに対し、型チェックが一切行われません。
TypeScriptの大きなメリットである型安全性を損なうため、基本的に使用は避けるべきです。
しかし、以下のようなやむを得ない状況では、一時的にany
型を使用することを検討できます。
- ライブラリやフレームワークとの連携: 型定義が提供されていない古いJavaScriptライブラリを使用する場合など、外部のコードと連携する際に、一時的にany型を使用せざるを得ないことがあります。
- プロトタイプ開発や移行作業: アプリケーションのごく一部で迅速に動作確認をしたい場合や、既存のJavaScriptコードを段階的にTypeScriptに移行する際など、一時的な回避策として利用されることがあります。
これらの場合でも、可能な限りunknown
型やジェネリクス、型アサーションなど、より安全な代替手段を検討し、any
型の使用範囲は最小限に留めることが重要です。
const anything: any = "Hello";
anything.toUpperCase(); // ✅ OK(エラーにならないが型チェックされない)
unknown
型 – どのような値も代入できる安全な型
unknown
型もany
型と同様にどのような値も代入できますが、型を絞り込まないと変数への代入やプロパティへのアクセス、メソッドの呼び出しがエラーになります。
const value: unknown = 'Taro';
// value.toUpperCase(); // ❌ エラー:Object is of type 'unknown'.
if (typeof value === 'string') {
console.log(value.toUpperCase()); // ✅ OK
}
void
型 – 何も返さない関数に使う型
void
型は関数が値を返さないことを型注釈するための型です。
function logMessage(): void {
console.log('Hello!');
}
never
型 – 絶対に戻ってこない関数に使う型
never
型は値を持たないことを意味する型です。
void
型は関数がreturn
されるか、最後まで実行される際に使うのに対し、never
型は関数が例外なくをスルーして中断されるか、永遠に実行される(無限ループなど)際に使用します。
function throwError(): never {
throw new Error('Something went wrong');
}
function infiniteLoop(): never {
while (true) {
// 🔁 終わらない
}
}
型エイリアス
既存の型に新しい名前を付ける機能です。type
キーワードを使用して定義します。
複雑な型をシンプルに表現したり、コードの可読性を向上させたりするのに役立ちます。
type UserID = string; // string型にUserIDという新しい名前を付けます
type Age = number; // number型にAgeという新しい名前を付けます
type UserProfile = {
// オブジェクトの型にもエイリアスを付けられます
id: UserID;
name: string;
age: Age;
isActive: boolean;
};
構造的部分型
TypeScript の型チェックの根幹をなす非常に重要な考え方です。
変数をある型から別の型に代入できるかどうかを判断する際に、その型の「中身(構造)」が互換性を持っているかどうかで決めます。
// Person型を定義
type Person = {
name: string;
age: number;
};
// Employee型を定義
type Employee = {
name: string;
age: number;
employeeId: string;
};
// Person型のオブジェクトを作成
const person1: Person = {
name: '田中',
age: 30,
};
// Employee型のオブジェクトを作成
const employee1: Employee = {
name: '佐藤',
age: 45,
employeeId: 'E001',
};
// 💡 Employee型はPerson型に必要なプロパティ(nameとage)をすべて持っているため、
// Person型として扱っても問題ないと判断されます。
const person2: Person = employee1; // ✅ OK
// 💡 逆に、Person型をEmployee型に代入しようとするとエラーになります。
// Person型にはemployeeIdプロパティがないため、Employee型としては不完全のためです。
const employee2: Employee = person1; // ❌ エラー:Property 'employeeId' is missing in type 'Person' but required in type 'Employee'.
配列
配列の型注釈
配列の型を指定するには、主に 2 つの方法があります。
-
型名[]
:一般的で直感的な記法です。 -
Array<型名>
:ジェネリック型を使った記法です。
// string型の配列を宣言する例
const fruits: string[] = ['apple', 'banana', 'cherry'];
// number型の配列を宣言する例
const numbers: Array<number> = [1, 2, 3, 4, 5];
// 💡 配列に異なる型の要素を入れようとするとエラーになります
const mixedArray: string[] = ['apple', 123]; // ❌ エラー:Type 'number' is not assignable to type 'string'.
読み取り専用配列
一度定義されたらその中の要素の変更ができない配列です。要素の追加、削除、変更といった操作が禁止されています。
意図しないデータの変更を防ぎ、コードの安全性を高めるために非常に役立ちます。
読み取り専用配列を宣言する方法も、主に 2 つあります。
-
readonly 型名[]
:配列の型注釈の前にreadonly
キーワードを付けます。 -
ReadonlyArray<型名>
:Array<型名>
と同様に、ジェネリック型としてReadonlyArray
を使用します。
// 読み取り専用のstring配列を宣言する例(readonly string[] 形式)
const colors: readonly string[] = ['red', 'green', 'blue'];
// 💡 読み取り専用なので、要素を変更しようとするとエラーになります
colors.push('yellow'); // ❌ エラー:Property 'push' does not exist on type 'readonly string[]'.
colors[0] = 'orange'; // ❌ エラー:Index signature in type 'readonly string[]' only permits reading.
タプル型
要素の数と、それぞれの要素の型が固定された配列を定義するための型です。
通常の配列が同じ型の要素をいくつでも格納できるのに対し、タプル型では要素の順番・型・配列の長さが厳密に決まっています。
関数の戻り値で複数の異なる型の値を返したい場合や、API からのレスポンスで特定の順序でデータが来ることが保証されている場合などに、その構造を明確にするために非常に便利です。
// 名前(string)と年齢(number)のタプル型を定義
type NameAndAge = [string, number];
// タプル型の変数に値を代入
const user1: NameAndAge = ['Alice', 30];
// 💡 要素の順番が違うとエラーになります
const user2: NameAndAge = [30, 'Bob']; // ❌ エラー:Type 'number' is not assignable to type 'string'.
// Type 'string' is not assignable to type 'number'.
// 💡 要素の数が違うとエラーになります
const user3: NameAndAge = ['Charlie', 25, true]; // ❌ エラー:Source has 3 elements, but target allows only 2.
オブジェクト
オブジェクトの型注釈
オブジェクトが持つプロパティ名と、それぞれのプロパティの型を { プロパティ名1: 型1, プロパティ名2: 型2, ... }
の形式で記述します。
定義した型にないプロパティを追加したり、必須プロパティが不足している場合にエラーになります。
// ユーザー情報を表すオブジェクトの型を定義
type User = {
name: string;
age: number;
isActive: boolean;
};
// 💡 定義した型にないプロパティを追加しようとするとエラーになります
const newUser: User = {
name: 'Bob',
age: 25,
isActive: false,
email: 'bob@example.com', // ❌ エラー:Object literal may only specify known properties, and 'email' does not exist in type 'User'.
};
// 💡 必須プロパティが不足している場合もエラーになります
const incompleteUser: User = {
name: 'Charlie',
age: 40,
// isActive プロパティが不足
}; // ❌ エラー:Property 'isActive' is missing in type '{ name: string; age: number; }' but required in type 'User'.
readonly
プロパティ
オブジェクトのプロパティに readonly
キーワードを付けると、そのプロパティは「読み取り専用」になります。
データの不変性を保証したい場合に非常に有効です。
// IDが読み取り専用のUser型を定義
type ImmutableUser = {
readonly id: string; // idプロパティは読み取り専用
name: string;
age: number;
};
const immutableUser: ImmutableUser = {
id: 'user-123',
name: 'David',
age: 28,
};
// 💡 readonlyプロパティは再代入できません
immutableUser.id = 'user-456'; // ❌ エラー:Cannot assign to 'id' because it is a read-only property.
// 💡 readonlyでないプロパティは変更可能です
immutableUser.name = 'David Smith'; // ✅ OK
オプションプロパティ
プロパティ名の後ろに ?
(クエスチョンマーク)を付けると、そのプロパティは「オプションプロパティ」になります。そのプロパティがオブジェクトに存在してもしなくても良いことを意味します。
// emailがオプションのUserProfile型を定義
type UserProfile = {
name: string;
age: number;
email?: string; // emailプロパティはあってもなくてもOK
};
インデックス型
オブジェクトが持つプロパティ名を事前にすべて決められない場合でも柔軟に型定義できます。
インデックス型プロパティの型注釈は、[キー名: プロパティキーの型]: プロパティ値の型
の形で記述します。
// 文字列をキーとし、値が数値であるオブジェクトの型を定義
type ScoreMap = {
[key: string]: number; // キーがstring型、値がnumber型
};
const scores: ScoreMap = {
math: 90,
english: 85,
science: 92,
};
Map
キーと値のペアを格納するコレクションで、あらゆる型の値をキーとして使用できます。
Map の型注釈
Map<キーの型, 値の型>
の形で記述します。
// stringをキー、numberを値とするMapの型を定義
const userScores: Map<string, number> = new Map();
// 値の追加
userScores.set('Alice', 95);
Set
重複しない値のコレクションです。
Set の型注釈
Set<要素の型>
の形で記述します。
// string型の要素を格納するSetの型を定義
const uniqueNames: Set<string> = new Set();
// 値の追加
uniqueNames.add('Alice');
列挙型(Enum)
関連する一連の定数に、分かりやすい名前を付けてグループ化するための機能です。
たとえば、曜日や月の名前、アプリケーションの状態など、事前に選択肢が決まっている値を扱うときに非常に役立ちます。
// 数値列挙型: デフォルトで数値が割り当てられる(0から始まる)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 明示的に値を指定した列挙型
enum HttpStatus {
OK = 200,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
}
// 文字列列挙型: 各メンバーに文字列を割り当てる
enum UserRole {
Admin = 'ADMIN',
Editor = 'EDITOR',
Viewer = 'VIEWER',
}
ユニオン型
TypeScript で「複数の型のうち、どれか 1 つである可能性がある値」を表現するための型です。
ユニオン型は 型1 | 型2 | ...
の形式で、各型をパイプ記号 |
で区切って記述します。この |
は「または(or)」という意味です。
// string型 または number型 を取りうる変数
type StringOrNumber = string | number;
let value: StringOrNumber;
value = 'こんにちは'; // ✅ OK
value = 123; // ✅ OK
判別可能なユニオン型
共通のプロパティ(判別子)を使って、ユニオン型の中から特定の型を識別できる特別なユニオン型です。
複雑な状態管理やイベント処理など、さまざまなシナリオでコードの堅牢性と可読性を向上させます。
// 判別子となる 'kind' プロパティを持つインターフェイスを定義
interface Circle {
kind: 'circle'; // 💡 ここが判別子!リテラル型で"circle"と固定
radius: number;
}
interface Square {
kind: 'square'; // 💡 こちらは"square"と固定
sideLength: number;
}
interface Triangle {
kind: 'triangle'; // 💡 こちらは"triangle"と固定
base: number;
height: number;
}
// これら3つのインターフェイスを組み合わせたユニオン型を定義
type Shape = Circle | Square | Triangle;
// 💡 判別可能なユニオン型を使った関数の例
function getArea(shape: Shape): number {
switch (
shape.kind // 'kind'プロパティをチェックすることで、TypeScriptが型を絞り込む
) {
case 'circle':
return Math.PI * shape.radius ** 2; // ✅ OK
case 'square':
return shape.sideLength * shape.sideLength; // ✅ OK
case 'triangle':
return (shape.base * shape.height) / 2; // ✅ OK
default:
// ありえないケースを防ぐための網羅性チェック
// たとえば、新しいShapeが追加されたのにここにcaseがないとエラーになる
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
インターセクション型
複数の型を 1つに結合し、すべての型の特徴を併せ持つ新しい型を定義する機能です。
インターセクション型は 型1 & 型2 & ...
の形式で、各型をアンパサンド &
で区切って記述します。この &
は「かつ(and)」という意味です。
ユニオン型が「A または B」だったのに対して、インターセクション型は「A かつ B」です。
// ユーザーの基本的な情報を持つ型
type Person = {
name: string;
age: number;
};
// 従業員としての情報を持つ型
type EmployeeDetails = {
employeeId: string;
department: string;
};
// 💡 Person型とEmployeeDetails型を結合したインターセクション型
// この型は、name, age, employeeId, departmentのすべてのプロパティを持つ必要がある
type Employee = Person & EmployeeDetails;
// Employee型に沿ったオブジェクトを作成
const myEmployee: Employee = {
name: '山田太郎',
age: 35,
employeeId: 'EMP-001',
department: '開発部',
};
制御フロー分析
コードの流れ(制御フロー)を分析して、自動的に変数の型を絞り込んでくれる機能です。この分析を助ける特定の手法を「型ガード」と呼びます。
型ガード
typeof
演算子
JavaScript にもともとある typeof
演算子は、変数の「プリミティブ型(string
, number
, boolean
, symbol
, bigint
, undefined
, function
, object
)」を文字列で返します。
function printLength(text: string | number) {
// 💡 ここでは 'text' は string | number 型
if (typeof text === 'string') {
// 💡 この 'if' ブロックの中では、'text' は string 型に絞り込まれます
console.log(`文字列の長さ: ${text.length}`); // ✅ OK
console.log(text.toFixed(2)); // ❌ エラー:Property 'toFixed' does not exist on type 'string'.
} else {
// 💡 こちらの 'else' ブロックの中では、'text' は number 型に絞り込まれます
console.log(`数値: ${text.toFixed(2)}`); // ✅ OK
console.log(text.length); // ❌ エラー:Property 'length' does not exist on type 'number'.
}
}
instanceof
演算子
あるオブジェクトが特定のクラスのインスタンスであるかどうかを判定するために使う型ガードです。
class Dog {
bark() {
console.log('ワンワン!');
}
}
class Cat {
meow() {
console.log('ニャーオ!');
}
}
type Animal = Dog | Cat; // Dog型またはCat型のユニオン型
function makeSound(animal: Animal) {
// 💡 instanceof を使って、animalがDogクラスのインスタンスか判定
if (animal instanceof Dog) {
// 💡 このブロック内では、animalはDog型に絞り込まれます
animal.bark(); // ✅ OK
}
// 💡 animalがCatクラスのインスタンスか判定
else if (animal instanceof Cat) {
// 💡 このブロック内では、animalはCat型に絞り込まれます
animal.meow(); // ✅ OK
}
}
in
演算子
オブジェクトが特定のプロパティを持っているかどうかを判定するために使う型ガードです。
クラスを使わずにオブジェクトリテラルで型を定義している場合や、特定のプロパティの有無で処理を分けたい場合に役立ちます。
type Car = {
drive(): void;
brand: string;
};
type Boat = {
sail(): void;
maxSpeed: number;
};
type Vehicle = Car | Boat; // Car型またはBoat型のユニオン型
function startVehicle(vehicle: Vehicle) {
// 💡 'drive'プロパティがvehicleオブジェクトに存在するか判定
if ('drive' in vehicle) {
// 💡 このブロック内では、vehicleはCar型に絞り込まれます
vehicle.drive(); // ✅ OK
console.log(`ブランド: ${vehicle.brand}`); // ✅ OK
}
// 💡 'sail'プロパティがvehicleオブジェクトに存在するか判定
else if ('sail' in vehicle) {
// 💡 このブロック内では、vehicleはBoat型に絞り込まれます
vehicle.sail(); // ✅ OK
console.log(`最高速度: ${vehicle.maxSpeed}ノット`); // ✅ OK
}
}
ユーザー定義の型ガード関数
独自の型ガードロジックを関数として定義し、再利用できるようにする機能です。
戻り値の型注釈に parameterName is Type
の形式を使うのが特徴です。その関数が true を返した場合に、引数 parameterName
が Type
に絞り込まれます。
// Person型を定義
interface Person {
name: string;
age: number;
}
// Admin型を定義(Person型にisAdminプロパティが追加された形)
interface Admin extends Person {
isAdmin: boolean;
}
// 💡 ユーザー定義の型ガード関数を定義
// 戻り値の型が 'value is Admin' となっているのがポイント
function isAdminUser(value: Person | Admin): value is Admin {
// valueがAdmin型であるかを判定するロジック
return 'isAdmin' in value && value.isAdmin === true;
}
関数
引数と戻り値の型を型注釈で明確に指定できます。
アロー関数の型注釈
引数の後ろに :
をつけて引数の型を書き、引数リストの ()
の後に再び :
をつけて戻り値の型を指定します。
// `num`はnumber型、戻り値もnumber型であることを示す
const increment = (num: number): number => num + 1;
関数宣言構文の型注釈
引数の後ろに :
をつけて引数の型を書き、関数の引数リスト ()
の直後に:
をつけて戻り値の型を指定します。
// `num`はnumber型、戻り値もnumber型であることを示す
function increment(num: number): number {
return num + 1;
}
オプション引数
引数名の後ろに ?
(クエスチョンマーク)を付けることで、その引数が任意であることを示します。
// オプション引数 `greeting` を持つ関数
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
} else {
return `Hello, ${name}!`;
}
}
クラス
クラス構文とフィールド宣言
name: string;
のようにフィールド(プロパティ)に型注釈を付けます。
// Userクラスを定義
class User {
name: string; // string型のフィールド
age: number; // number型のフィールド
// コンストラクター: インスタンスが作られるときに呼ばれる
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
readonly
修飾子
クラスの readonly
修飾子をつけたフィールド(プロパティ)は、インスタンスが作成される際の初期化時(コンストラクター内)にのみ値を設定でき、それ以降は変更できない読み取り専用のプロパティになります。
public
や private
といったアクセス修飾子と併用することも可能です。
class Product {
readonly id: string; // 読み取り専用のID
public name: string; // publicで読み書き可能
private price: number; // privateで外部から直接アクセス不可
constructor(id: string, name: string, price: number) {
this.id = id; // 💡 コンストラクター内でのみreadonlyプロパティを初期化できます
this.name = name;
this.price = price;
}
}
Constructor shorthand
コンストラクターのパラメーターに直接、public
、private
、protected
、またはreadonly
といったアクセス修飾子を付けることで、以下の 3 つの処理が自動的に実行されます。
- 同じ名前のフィールド(プロパティ)を自動で定義する。
- そのフィールドに、指定されたアクセス修飾子を適用する。
- コンストラクターの引数の値を、自動でそのフィールドに代入する。
これによりコードの簡略化が図れます。
// 通常のクラスの書き方
class OldUser {
name: string; // ① プロパティの宣言
age: number;
constructor(name: string, age: number) {
this.name = name; // ② コンストラクター内で引数をプロパティに代入
this.age = age;
}
}
// コンストラクター省略記法を使った書き方
class NewUser {
// 💡 コンストラクターのパラメーターに直接アクセス修飾子をつけます
constructor(public name: string, private age: number, readonly id: string) {
// この中に明示的な代入は不要!
// name, age, id は自動的にフィールドとして定義され、値が代入されます
}
}
abstract
抽象クラス abstract
キーワードを使って定義できる直接インスタンス化することができないクラスです。
他のクラスに継承されて使われることを前提としています。
// 抽象クラス Animal を定義
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
// 💡 抽象メソッド: 実装を持たず、継承先で実装が必須です
abstract makeSound(): void;
// 通常のメソッド: 共通の処理をここに書けます
move(): void {
console.log(`${this.name} は動きます。`);
}
}
// Animalを継承したDogクラス
class Dog extends Animal {
constructor(name: string) {
super(name);
}
// 💡 抽象メソッド makeSound を実装する必要があります
makeSound(): void {
console.log(`${this.name} がワンワンと吠えます。`);
}
}
interface
インターフェイス 特定のオブジェクトやクラスが持つべきプロパティやメソッドの名前と型だけを記述し、具体的な実装は持たないのが特徴です。
複数のクラスに共通の振る舞いを強制したり、関数が受け取るオブジェクトの構造を定義したりする際に非常に役立ちます。
// Personインターフェイスを定義
interface Person {
name: string; // nameプロパティはstring型
age: number; // ageプロパティはnumber型
greet(): string; // greetメソッドはstringを返す
}
// 💡 Personインターフェイスを実装するクラス
// implementキーワードを使って、インターフェイスの契約を満たすことを宣言します
class User implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `こんにちは、${this.name}です!`;
}
}
readonly
修飾子
インターフェイスの インターフェイス内で readonly
修飾子を使用して、プロパティを読み取り専用に設定できます。
そのインターフェイスを実装またはその型を持つオブジェクトにおいて、一度初期化されたらその値を変更することができません。
// 💡 idが読み取り専用のProductインターフェイスを定義
interface Product {
readonly id: string; // idは読み取り専用
name: string;
price: number;
}
// Productインターフェイスを実装するクラス
class ConcreteProduct implements Product {
readonly id: string;
name: string;
price: number;
constructor(id: string, name: string, price: number) {
this.id = id; // 💡 readonlyプロパティはコンストラクターで初期化できます
this.name = name;
this.price = price;
}
}
非同期処理
Promise
TypeScript で Promise の型を扱う際は、ジェネリック型を使って、Promise<解決される値の型>
の形式で指定します。
// string型の値を解決するPromise
function fetchData(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve('データ取得成功!');
}, 1000); // 1秒後に解決
});
}
まとめ
今回の記事では、プリミティブ型、配列、タプル型、オブジェクト型、関数、クラス、非同期処理など基本的な型から応用的な機能までを網羅しました。
次回の記事は TypeScript のさらに強力な機能である「ジェネリクス(Generics)」について深掘りしていきます。
参考リンク
Discussion