JavaScriptで開発をしていると、「undefinedのプロパティを読み取れない」「引数の型が違う」といった実行時エラーに悩まされた経験はないでしょうか。TypeScriptは、JavaScriptに静的型付けを追加した言語であり、こうしたエラーをコードを書いている段階で検出できます。
この記事では、TypeScriptの型の書き方を基本から応用まで体系的に解説します。stringやnumberといった基本型から、Union型・Generics・型ガード・ユーティリティ型・Mapped Types・Conditional Typesまで、実務で必要な知識をすべてカバーします。
この記事で学べること
- TypeScriptの型システムの全体像と基本の型(string, number, boolean, any, unknown, never等)
- 配列・タプル・オブジェクト型・interface・type aliasの使い分け
- Union型・Intersection型・リテラル型・テンプレートリテラル型
- 型推論・型アサーション・型ガード(typeof, instanceof, in, ユーザー定義型ガード)
- Generics(ジェネリクス)の基礎から実践パターン
- ユーティリティ型(Partial, Required, Pick, Omit, Record, Readonly等)
- Mapped Types・Conditional Typesの仕組みと活用法
- 実務で使う型パターン集(APIレスポンス型、イベントハンドラー型等)
- よくあるエラーと対処法
TypeScriptとは ― JavaScriptとの違い
TypeScriptは、Microsoftが開発したJavaScriptのスーパーセット(上位互換)言語です。JavaScriptの構文をすべて含みつつ、静的型付けの機能を追加しています。TypeScriptで書いたコードはトランスパイル(変換)されて通常のJavaScriptとして実行されます。
| 比較項目 |
JavaScript |
TypeScript |
| 型システム |
動的型付け |
静的型付け + 動的型付け |
| エラー検出 |
実行時 |
コンパイル時(コーディング中) |
| 実行環境 |
ブラウザ / Node.js で直接実行 |
JSに変換してから実行 |
| IDE支援 |
基本的な補完 |
強力な補完・リファクタリング |
| 学習コスト |
低い |
やや高い(型の知識が必要) |
| 拡張子 |
.js |
.ts / .tsx |
なぜ型が必要なのか
JavaScriptは動的型付け言語です。変数にどんな値でも代入できる自由さがある一方、型に起因するバグが実行時まで発見できません。
JavaScriptの場合(実行時にエラー)
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet(42); // TypeError: name.toUpperCase is not a function
TypeScriptの場合(コンパイル時にエラー)
function greet(name: string) {
return "Hello, " + name.toUpperCase();
}
greet(42); // コンパイルエラー: Argument of type 'number' is not assignable to parameter of type 'string'
ポイント:TypeScriptの型はコンパイル時のみ存在し、実行時のJavaScriptには残りません。つまり型を付けてもパフォーマンスへの影響はゼロです。
環境構築
TypeScriptの開発環境を構築しましょう。Node.jsがインストールされていることが前提です。
TypeScriptのインストール
ターミナル
# プロジェクト初期化
mkdir my-ts-project && cd my-ts-project
npm init -y
# TypeScriptをdevDependenciesに追加
npm install --save-dev typescript
# バージョン確認
npx tsc --version
tsconfig.json の作成
tsconfig.jsonはTypeScriptコンパイラの設定ファイルです。以下のコマンドで初期設定を生成できます。
生成されるファイルにはたくさんのオプションがコメントアウトで含まれていますが、実務でよく使う設定は以下のようなものです。
tsconfig.json
{
"compilerOptions": {
"target": "ES2020", // 出力するJSのバージョン
"module": "commonjs", // モジュールシステム
"strict": true, // 厳格な型チェックを有効化
"esModuleInterop": true, // ESM/CJSの相互運用
"outDir": "./dist", // 出力先ディレクトリ
"rootDir": "./src", // ソースのルート
"resolveJsonModule": true, // JSONファイルのimport
"declaration": true, // 型定義ファイル(.d.ts)を生成
"sourceMap": true // ソースマップを生成
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
注意:"strict": true は strictNullChecks、noImplicitAny、strictFunctionTypes など複数の厳格チェックをまとめて有効化します。新規プロジェクトでは必ず有効にしましょう。
コンパイルと実行
ターミナル
# コンパイル(.ts → .js)
npx tsc
# ウォッチモード(ファイル変更を監視して自動コンパイル)
npx tsc --watch
# ts-nodeで直接実行(開発時に便利)
npm install --save-dev ts-node
npx ts-node src/index.ts
基本の型(プリミティブ型)
TypeScriptの型注釈(Type Annotation)は、変数名の後に : 型名 の形式で記述します。まずはJavaScriptのプリミティブ値に対応する基本型を見ていきましょう。
string(文字列)
string型
let greeting: string = "Hello, TypeScript";
let userName: string = "太郎";
let template: string = `こんにちは、${userName}さん`; // テンプレートリテラルもOK
// エラー: number型を代入しようとした
let wrong: string = 123; // Type 'number' is not assignable to type 'string'
number(数値)
number型
let age: number = 25;
let price: number = 19.99;
let hex: number = 0xff; // 16進数
let binary: number = 0b1010; // 2進数
let inf: number = Infinity;
let notNum: number = NaN; // NaNもnumber型
boolean(真偽値)
boolean型
let isActive: boolean = true;
let hasPermission: boolean = false;
// 関数の戻り値にも使える
function isAdult(age: number): boolean {
return age >= 18;
}
null と undefined
nullとundefinedはそれぞれ独立した型を持ちます。strictNullChecksが有効な場合(推奨)、他の型にnullやundefinedを代入することはできません。
null / undefined
let nothing: null = null;
let notDefined: undefined = undefined;
// strictNullChecks: true の場合
let str: string = null; // エラー!
let num: number = undefined; // エラー!
// nullを許容したい場合はUnion型を使う
let nullable: string | null = null; // OK
let optional: number | undefined = undefined; // OK
any(なんでも型)
any型はすべての型チェックを無効化します。JavaScriptからの移行時には便利ですが、TypeScriptの恩恵を完全に失うため、極力使わないようにしましょう。
any型(非推奨)
let anything: any = 42;
anything = "hello"; // OK(型チェックなし)
anything = true; // OK
anything.foo.bar.baz; // OK(実行時にエラーになる可能性あり)
注意:anyは型安全性を完全に放棄します。「型がわからない」場合は、後述するunknown型を使いましょう。ESLintの@typescript-eslint/no-explicit-anyルールでanyの使用を禁止できます。
unknown(安全ななんでも型)
unknown型はanyと同じくすべての値を受け入れますが、使用する前に型チェックが必要です。型安全なanyの代替として使いましょう。
unknown型
let value: unknown = "hello";
// そのままでは操作できない
value.toUpperCase(); // エラー: Object is of type 'unknown'
// 型チェック(型ガード)を行えば使える
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK: "HELLO"
}
void(戻り値なし)
void型は、関数が値を返さないことを示します。
void型
function logMessage(msg: string): void {
console.log(msg);
}
const warn = (msg: string): void => {
console.warn(msg);
};
never(到達不可能な型)
never型は決して値を返さない関数の戻り値型です。例外をスローする関数や無限ループに使います。
never型
function throwError(msg: string): never {
throw new Error(msg);
}
// 網羅性チェック(Exhaustive Check)
type Color = "red" | "blue";
function handleColor(c: Color) {
switch (c) {
case "red": return "#ff0000";
case "blue": return "#0000ff";
default:
const _exhaustive: never = c;
return _exhaustive;
}
}
基本型の一覧
| 型名 |
説明 |
例 |
string |
文字列 |
"hello" |
number |
数値(整数・小数) |
42, 3.14 |
boolean |
真偽値 |
true, false |
null |
値がないことを示す |
null |
undefined |
未定義 |
undefined |
any |
型チェック無効(非推奨) |
なんでもOK |
unknown |
安全なany(型チェック必須) |
APIレスポンス等 |
void |
戻り値なし |
console.log等 |
never |
到達不可能 |
例外スロー |
配列・タプルの型
配列型(Array)
配列の型を宣言するには2つの書き方があります。どちらも同じ意味ですが、型名[]の方が一般的です。
配列型の宣言
// 書き方1: 型名[](推奨)
let numbers: number[] = [1, 2, 3];
let names: string[] = ["Alice", "Bob"];
// 書き方2: Array<型名>(ジェネリクス記法)
let scores: Array<number> = [90, 85, 70];
// 型が違う要素は追加できない
numbers.push("four"); // エラー
// 読み取り専用の配列
const readonlyArr: readonly number[] = [1, 2, 3];
readonlyArr.push(4); // エラー: Property 'push' does not exist
タプル型(Tuple)
タプルは要素の数と各位置の型が固定された配列です。関数から複数の値を返すときに便利です。
タプル型
// [string, number] の順番・型が固定
let user: [string, number] = ["Alice", 30];
// 順番を間違えるとエラー
let wrong: [string, number] = [30, "Alice"]; // エラー
// オプション要素
let pair: [string, number?] = ["hello"]; // OK
// 可変長タプル(Rest Elements)
type StrNumBools = [string, number, ...boolean[]];
const example: StrNumBools = ["hi", 1, true, false];
// 名前付きタプル(TS 4.0+)
type Range = [start: number, end: number];
const range: Range = [0, 100];
ポイント:ReactのuseStateフック const [count, setCount] = useState(0) は、タプル型 [number, Dispatch<SetStateAction<number>>] を返しています。タプルの分割代入は実務で頻繁に登場します。
オブジェクト型・interface・type alias
TypeScriptでオブジェクトの型を定義する方法は複数あります。ここではオブジェクト型リテラル、interface、type aliasの3つを解説します。
オブジェクト型リテラル
変数宣言時に直接オブジェクトの構造を記述する方法です。
オブジェクト型リテラル
// オブジェクトの形をそのまま型として記述
let user: { name: string; age: number } = {
name: "太郎",
age: 25
};
// オプショナルプロパティ(?)
let config: { host: string; port?: number } = {
host: "localhost"
// port は省略可能
};
// 読み取り専用プロパティ(readonly)
let point: { readonly x: number; readonly y: number } = { x: 10, y: 20 };
point.x = 30; // エラー: Cannot assign to 'x' because it is a read-only property
interface(インターフェース)
interfaceはオブジェクトの型に名前を付けて再利用可能にする仕組みです。クラスの実装契約としても使えます。
interface
interface User {
name: string;
age: number;
email?: string; // オプション
readonly id: number; // 読み取り専用
}
const user: User = {
name: "太郎",
age: 25,
id: 1
};
// メソッドの定義
interface Calculator {
add(a: number, b: number): number;
subtract(a: number, b: number): number;
}
// interfaceの拡張(extends)
interface Admin extends User {
role: "admin";
permissions: string[];
}
// 宣言マージ(同名のinterfaceは自動的に結合される)
interface Window {
myCustomProp: string;
}
// ← 元のWindow型にmyCustomPropが追加される
type alias(型エイリアス)
typeキーワードで型に名前を付けます。interfaceと似ていますが、Union型やプリミティブ型にも名前を付けられる点が異なります。
type alias
// オブジェクト型
type User = {
name: string;
age: number;
};
// プリミティブ型にも名前を付けられる
type ID = string | number;
type Status = "active" | "inactive" | "pending";
// 関数型
type Callback = (data: string) => void;
// タプル型
type Coordinate = [number, number];
// 交差型(Intersection)
type Admin = User & { role: "admin" };
interface と type の使い分け
| 機能 |
interface |
type |
| オブジェクト型の定義 |
○ |
○ |
| Union型の定義 |
× |
○ |
| プリミティブ型への命名 |
× |
○ |
| extends(拡張) |
○ |
○(&で交差) |
| 宣言マージ |
○ |
× |
| implements(クラス実装) |
○ |
○ |
| Mapped Types |
× |
○ |
ポイント:一般的な使い分けとして、オブジェクトの構造を定義する場合はinterface、Union型やプリミティブ型に名前を付ける場合はtypeを使うのがおすすめです。プロジェクト内で統一していれば、どちらを使っても大きな問題はありません。
インデックスシグネチャ
プロパティ名が動的に決まるオブジェクトの型を定義するには、インデックスシグネチャを使います。
インデックスシグネチャ
// キーがstring、値がnumberのオブジェクト
interface ScoreMap {
[subject: string]: number;
}
const scores: ScoreMap = {
math: 90,
english: 85,
science: 78
};
// 固定プロパティとの併用
interface Config {
version: number; // 必須プロパティ
[key: string]: unknown; // それ以外は任意
}
Union型・Intersection型
TypeScriptでは、複数の型を組み合わせて新しい型を作ることができます。Union型は「いずれかの型」、Intersection型は「すべての型を満たす型」です。
Union型( |)
Union型は |(パイプ)で複数の型をつなぎ、そのどれか1つに当てはまることを表します。
Union型
// string または number
let id: string | number;
id = "abc"; // OK
id = 123; // OK
id = true; // エラー: Type 'boolean' is not assignable
// 関数の引数に使う
function printId(id: string | number) {
// 共通のメソッドしか使えない
console.log(id.toString()); // OK(両方にある)
// 型を絞り込めば固有のメソッドが使える
if (typeof id === "string") {
console.log(id.toUpperCase()); // OK
}
}
// 配列との組み合わせ
let arr: (string | number)[] = ["hello", 42, "world"];
// null許容型(よく使うパターン)
type Nullable<T> = T | null;
let username: Nullable<string> = null; // OK
Intersection型( & )
Intersection型は & で複数の型をつなぎ、すべての型のプロパティを持つ型を作ります。
Intersection型
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
// すべてのプロパティが必要
type Person = HasName & HasAge & HasEmail;
const person: Person = {
name: "太郎",
age: 25,
email: "taro@example.com"
};
// 実務例: 既存の型に機能を追加
type Timestamped<T> = T & {
createdAt: Date;
updatedAt: Date;
};
type TimestampedUser = Timestamped<User>;
注意:Intersection型でプリミティブ型同士を交差させるとneverになります。例えば string & number は never です(stringでありかつnumberである値は存在しないため)。Intersection型はオブジェクト型の合成に使いましょう。
リテラル型・テンプレートリテラル型
リテラル型
リテラル型は、特定の値そのものを型として使う機能です。string、number、booleanのリテラル型があります。
リテラル型
// 文字列リテラル型
type Direction = "up" | "down" | "left" | "right";
function move(direction: Direction) {
console.log(`Moving ${direction}`);
}
move("up"); // OK
move("diagonal"); // エラー: Argument of type '"diagonal"' is not assignable
// 数値リテラル型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
// HTTPステータスコード
type SuccessCode = 200 | 201 | 204;
type ErrorCode = 400 | 401 | 403 | 404 | 500;
type StatusCode = SuccessCode | ErrorCode;
// constアサーションでリテラル型を推論させる
const config = {
endpoint: "/api/users",
method: "GET"
} as const;
// config.method の型は "GET"(stringではない)
テンプレートリテラル型(TS 4.1+)
テンプレートリテラル型は、文字列リテラル型をテンプレート構文で組み合わせて新しい型を生成します。
テンプレートリテラル型
// 基本的な連結
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
// Union型との組み合わせ(直積)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Color}-${Size}`;
// "red-small" | "red-large" | "blue-small" | "blue-large"
// CSSプロパティ名の生成
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSSpacing = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left" (8通り)
// イベントハンドラー名の生成
type EventName = "click" | "scroll" | "mousemove";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onScroll" | "onMousemove"
組み込み文字列操作型
Uppercase<T> ― 全て大文字に変換
Lowercase<T> ― 全て小文字に変換
Capitalize<T> ― 先頭を大文字に変換
Uncapitalize<T> ― 先頭を小文字に変換
型推論(Type Inference)
TypeScriptは多くの場面で型を自動的に推論してくれるため、すべての変数に型注釈を書く必要はありません。これを型推論(Type Inference)と呼びます。
型推論の例
// 変数の初期化時に推論される
let message = "hello"; // string と推論
let count = 42; // number と推論
let isValid = true; // boolean と推論
let items = [1, 2, 3]; // number[] と推論
// constで宣言するとリテラル型になる
const greeting = "hello"; // "hello" と推論(リテラル型)
const PI = 3.14; // 3.14 と推論
// 関数の戻り値も推論される
function add(a: number, b: number) {
return a + b; // 戻り値は number と推論
}
// コールバック関数のパラメータも推論される
const nums = [1, 2, 3];
nums.map((n) => n * 2); // n は number と推論
// 分割代入でも推論される
const { name, age } = { name: "太郎", age: 25 };
// name: string, age: number
ポイント:型推論が効く場面では型注釈を省略してコードを簡潔に保ちましょう。ただし、関数の引数や公開APIの戻り値には明示的に型を書くことが推奨されます。
型アサーション(Type Assertion)
型アサーションは、TypeScriptのコンパイラに対して「この値はこの型だと私は知っている」と伝える機能です。コンパイラの推論を上書きしますが、実行時の変換は行いません。
型アサーション
// as 構文(推奨)
const input = document.getElementById("myInput") as HTMLInputElement;
console.log(input.value); // HTMLInputElementとして扱える
// アングルブラケット構文(JSXでは使えない)
const input2 = <HTMLInputElement>document.getElementById("myInput");
// JSONパース結果の型指定
interface ApiResponse {
data: string[];
total: number;
}
const response = JSON.parse(jsonStr) as ApiResponse;
// constアサーション(値をリテラル型にする)
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = (typeof colors)[number]; // "red" | "green" | "blue"
注意:型アサーションは型チェックを迂回するため、誤った型を指定すると実行時エラーの原因になります。as anyは特に危険です。型アサーションの代わりに型ガード(次セクション)を使えないか検討しましょう。
Non-null アサーション演算子(!)
Non-null アサーション
// nullまたはundefinedではないと断言
const el = document.getElementById("app")!; // HTMLElement(nullではない)
// ただし要素が存在しない場合は実行時エラーになる
// 安全なのはnullチェックを行う方法
const el2 = document.getElementById("app");
if (el2) {
el2.textContent = "Hello"; // 安全
}
型ガード(Type Guard)
型ガードは、条件分岐によって変数の型を絞り込む仕組みです。Union型の値を安全に扱うために不可欠なテクニックです。
typeof による型ガード
typeof
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
// ここでは padding は number 型
return " ".repeat(padding) + value;
}
// ここでは padding は string 型
return padding + value;
}
padLeft("hello", 4); // " hello"
padLeft("hello", ">> "); // ">> hello"
instanceof による型ガード
instanceof
class Dog {
bark() { return "ワン!"; }
}
class Cat {
meow() { return "ニャー!"; }
}
function makeSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return animal.bark(); // Dog型に絞り込まれる
}
return animal.meow(); // Cat型に絞り込まれる
}
in 演算子による型ガード
in演算子
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Rectangle;
function getArea(shape: Shape): number {
if ("radius" in shape) {
// Circle型に絞り込まれる
return Math.PI * shape.radius ** 2;
}
// Rectangle型に絞り込まれる
return shape.width * shape.height;
}
判別可能なUnion型(Discriminated Union)
各型に共通の判別プロパティ(discriminant)を持たせて、switch文で型を絞り込むパターンです。実務で最も多用されます。
Discriminated Union
type Result =
| { status: "success"; data: string }
| { status: "error"; error: Error }
| { status: "loading" };
function handleResult(result: Result) {
switch (result.status) {
case "success":
console.log(result.data); // data にアクセス可能
break;
case "error":
console.error(result.error); // error にアクセス可能
break;
case "loading":
console.log("Loading...");
break;
}
}
ユーザー定義型ガード
独自の型チェック関数を作るには、戻り値の型に 引数名 is 型 と書きます。
ユーザー定義型ガード
interface Fish { swim: () => void; }
interface Bird { fly: () => void; }
// 戻り値の型が「pet is Fish」(型述語)
function isFish(pet: Fish | Bird): pet is Fish {
return ("swim" in pet);
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // Fish型に絞り込まれる
} else {
pet.fly(); // Bird型に絞り込まれる
}
}
// 実務例: nullishチェック関数
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
const items = ["a", null, "b", undefined, "c"];
const filtered = items.filter(isDefined); // string[] に絞り込まれる
Generics(ジェネリクス)基礎から実践
Genericsは、型をパラメータとして受け取る仕組みです。関数やクラスを作る時点では型を決めず、使う時点で型を指定します。これにより、型安全性を保ちつつ再利用可能なコードが書けます。
ジェネリック関数の基本
ジェネリック関数
// anyを使うと型情報が失われる(NG)
function firstAny(arr: any[]): any {
return arr[0];
}
// Genericsを使えば型情報を保持できる(OK)
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 型パラメータTは呼び出し時に推論される
const num = first([1, 2, 3]); // number | undefined
const str = first(["a", "b", "c"]); // string | undefined
// 明示的に型パラメータを指定することもできる
const val = first<string>(["hello"]); // string | undefined
複数の型パラメータ
複数の型パラメータ
// キーと値のペアを返す関数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair("name", "太郎"); // [string, string]
const p2 = pair("age", 25); // [string, number]
// マップ変換関数
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
const lengths = mapArray(["hello", "world"], s => s.length);
// number[]
ジェネリック制約(extends)
型パラメータに制約を付けることで、特定のプロパティやメソッドを持つ型のみを受け入れるようにできます。
ジェネリック制約
// lengthプロパティを持つ型のみ受け入れる
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
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 };
getProperty(user, "name"); // string
getProperty(user, "age"); // number
getProperty(user, "email"); // エラー: "email"はkeyof Userに含まれない
ジェネリックインターフェース・型エイリアス
ジェネリック型の定義
// ジェネリックインターフェース
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// 使用時に型を指定
const userRes: ApiResponse<User> = {
data: { name: "太郎", age: 25 },
status: 200,
message: "OK"
};
const listRes: ApiResponse<string[]> = {
data: ["item1", "item2"],
status: 200,
message: "OK"
};
// デフォルト型パラメータ
interface Pagination<T = unknown> {
items: T[];
page: number;
totalPages: number;
}
// ジェネリック型エイリアス
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
ジェネリッククラス
ジェネリッククラス
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push("three"); // エラー: Argument of type 'string'
const strStack = new Stack<string>();
strStack.push("hello");
ポイント:型パラメータの命名規則として、T(Type)、K(Key)、V(Value)、E(Element/Error)が慣習的に使われます。複数の型パラメータがある場合は意味のある名前を付けると可読性が上がります。
ユーティリティ型(Utility Types)
TypeScriptには、既存の型を変換して新しい型を作る組み込みユーティリティ型が豊富に用意されています。これらを活用すると、冗長な型定義を避けてDRY(Don’t Repeat Yourself)なコードが書けます。
Partial<T> ― すべてのプロパティをオプショナルに
Partial
interface User {
name: string;
age: number;
email: string;
}
// すべてのプロパティがオプショナルになる
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }
// 実務例: 更新関数の引数(一部だけ更新したい)
function updateUser(id: number, updates: Partial<User>): User {
const currentUser = getUser(id);
return { ...currentUser, ...updates };
}
updateUser(1, { name: "新しい名前" }); // OK: ageとemailは省略可
Required<T> ― すべてのプロパティを必須に
Required
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// すべて必須になる
type FullConfig = Required<Config>;
// { host: string; port: number; debug: boolean; }
Pick<T, K> ― 特定のプロパティだけ抽出
Pick
interface User {
id: number;
name: string;
email: string;
password: string;
}
// nameとemailだけ取り出す
type UserPreview = Pick<User, "name" | "email">;
// { name: string; email: string; }
Omit<T, K> ― 特定のプロパティを除外
Omit
// passwordを除外
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; }
// 新規作成時はidが不要
type CreateUserInput = Omit<User, "id">;
// { name: string; email: string; password: string; }
Record<K, V> ― キーと値の型を指定した辞書型
Record
// キーがstring、値がnumber
const scores: Record<string, number> = {
math: 90,
english: 85,
science: 78
};
// リテラル型と組み合わせて、キーを限定する
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"]
};
Readonly<T> ― すべてのプロパティを読み取り専用に
Readonly
const user: Readonly<User> = {
id: 1,
name: "太郎",
email: "taro@example.com",
password: "secret"
};
user.name = "次郎"; // エラー: Cannot assign to 'name'
その他のユーティリティ型
Extract / Exclude / NonNullable / ReturnType / Parameters
// Extract: Union型から特定の型を抽出
type T1 = Extract<"a" | "b" | "c", "a" | "b">;
// "a" | "b"
// Exclude: Union型から特定の型を除外
type T2 = Exclude<"a" | "b" | "c", "a">;
// "b" | "c"
// NonNullable: null と undefined を除外
type T3 = NonNullable<string | null | undefined>;
// string
// ReturnType: 関数の戻り値の型を取得
function createUser() {
return { id: 1, name: "太郎" };
}
type CreatedUser = ReturnType<typeof createUser>;
// { id: number; name: string; }
// Parameters: 関数のパラメータ型をタプルで取得
function greet(name: string, age: number) {}
type GreetParams = Parameters<typeof greet>;
// [string, number]
// Awaited: Promiseの中身の型を取得(TS 4.5+)
type T4 = Awaited<Promise<string>>;
// string
ユーティリティ型の一覧
| ユーティリティ型 |
説明 |
Partial<T> |
全プロパティをオプショナルに |
Required<T> |
全プロパティを必須に |
Readonly<T> |
全プロパティを読み取り専用に |
Pick<T, K> |
指定したプロパティだけ抽出 |
Omit<T, K> |
指定したプロパティを除外 |
Record<K, V> |
キーKと値Vの辞書型を作成 |
Extract<T, U> |
TからUに代入可能な型を抽出 |
Exclude<T, U> |
TからUに代入可能な型を除外 |
NonNullable<T> |
null/undefinedを除外 |
ReturnType<T> |
関数型Tの戻り値の型を取得 |
Parameters<T> |
関数型Tのパラメータ型をタプルで取得 |
Awaited<T> |
Promiseの中身の型を取得 |
Mapped Types(マップ型)
Mapped Typesは、既存の型のプロパティを一括で変換して新しい型を生成する仕組みです。実はPartialやReadonlyなどのユーティリティ型はMapped Typesで実装されています。
Mapped Typesの基本構文
// Mapped Typesの基本構文
// { [P in K]: T }
// Partialの内部実装
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// Readonlyの内部実装
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// 全プロパティの値をstring型にする
type Stringify<T> = {
[P in keyof T]: string;
};
interface User {
name: string;
age: number;
active: boolean;
}
type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string; }
キーの再マッピング(Key Remapping, TS 4.1+)
Key Remapping
// Getter型を自動生成
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// getActive: () => boolean;
// }
// 特定のプロパティを除外(as neverでフィルタリング)
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P];
};
type WithoutBooleans = OmitByType<User, boolean>;
// { name: string; age: number; }
Conditional Types(条件付き型)
Conditional Typesは、型レベルのif文です。条件に基づいて異なる型を返します。
Conditional Typesの基本
// 基本構文: T extends U ? X : Y
// 型が文字列かどうかを判定
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// 配列の要素型を取得
type ElementOf<T> = T extends (infer E)[] ? E : never;
type C = ElementOf<string[]>; // string
type D = ElementOf<number[]>; // number
type E = ElementOf<string>; // never(配列ではない)
infer キーワード
inferはConditional Types内で型を推論して変数に束縛するキーワードです。
infer キーワード
// 関数の戻り値型を取得(ReturnTypeの実装)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Promiseの中身を取得
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type F = UnwrapPromise<Promise<string>>; // string
type G = UnwrapPromise<number>; // number(Promiseではない)
// 配列の最初の要素の型を取得
type Head<T extends any[]> = T extends [infer First, ...any[]] ? First : never;
type H = Head<[string, number, boolean]>; // string
ポイント:Mapped TypesやConditional Typesは高度な機能ですが、ライブラリの型定義を読む際に頻繁に登場します。まずはユーティリティ型を使いこなし、その内部実装を理解していくと自然に身に付きます。
実務でよく使う型パターン集
ここでは、実際のプロジェクトで頻繁に登場する型パターンを紹介します。コピーして即座に使えるように設計しています。
APIレスポンス型
APIレスポンス型の設計パターン
// 成功・失敗を型で表現(Discriminated Union)
type ApiResponse<T> =
| { success: true; data: T; }
| { success: false; error: { code: number; message: string; }; };
// ページネーション付きレスポンス
interface PaginatedResponse<T> {
items: T[];
pagination: {
page: number;
perPage: number;
total: number;
totalPages: number;
};
}
// fetch関数のラッパー
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
try {
const res = await fetch(url);
const data = await res.json() as T;
return { success: true, data };
} catch (e) {
return { success: false, error: { code: 500, message: String(e) } };
}
}
// 使用例
const result = await fetchApi<User[]>("/api/users");
if (result.success) {
console.log(result.data); // User[]
} else {
console.error(result.error.message); // string
}
イベントハンドラー型
イベントハンドラー型(React風)
// DOM イベント
const handleClick = (e: MouseEvent) => {
console.log(e.clientX, e.clientY);
};
const handleInput = (e: Event) => {
const target = e.target as HTMLInputElement;
console.log(target.value);
};
// React のイベント型
// import { ChangeEvent, FormEvent } from "react";
// const handleChange = (e: ChangeEvent<HTMLInputElement>) => { ... };
// const handleSubmit = (e: FormEvent<HTMLFormElement>) => { ... };
// カスタムイベント型
type EventMap = {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string };
pageView: { path: string; referrer?: string };
};
function emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
// ...
}
emit("userLogin", { userId: "123", timestamp: new Date() }); // OK
emit("pageView", { path: "/home" }); // OK
emit("userLogin", { path: "/home" }); // エラー
状態管理の型パターン
非同期データの状態管理型
// 非同期データの4つの状態を型で表現
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
// 使用例
function renderUsers(state: AsyncState<User[]>) {
switch (state.status) {
case "idle":
return "初期状態";
case "loading":
return "読み込み中...";
case "success":
return state.data.map(u => u.name);
case "error":
return `エラー: ${state.error}`;
}
}
フォームバリデーション型
フォームバリデーション型
// フォームのフィールドエラー型
type FormErrors<T> = {
[K in keyof T]?: string;
};
interface SignUpForm {
username: string;
email: string;
password: string;
}
const errors: FormErrors<SignUpForm> = {
email: "無効なメールアドレスです",
password: "8文字以上にしてください"
// username は省略可能
};
// バリデーション関数の型
type Validator<T> = (value: T) => string | null;
const required: Validator<string> = (v) =>
v.length === 0 ? "入力必須です" : null;
const minLength = (min: number): Validator<string> =>
(v) => v.length < min ? `${min}文字以上にしてください` : null;
Branded Types(ブランド型)
同じプリミティブ型(string)でも意味が異なる値を型レベルで区別するテクニックです。
Branded Types
// ブランドを付与する型
type Brand<T, B> = T & { __brand: B };
// UserIdとPostIdは両方stringだが、混同できない
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = "user_123" as UserId;
const postId = "post_456" as PostId;
getUser(userId); // OK
getUser(postId); // エラー: PostIdはUserIdに代入できない
よくあるエラーと対処法
TypeScriptで開発を始めると、型に関するエラーメッセージに戸惑うことがあります。ここでは頻出のエラーとその対処法をまとめます。
| エラーメッセージ |
原因 |
対処法 |
Type 'X' is not assignable to type 'Y' |
型Xを型Yに代入しようとした |
代入先の型を確認し、値を正しい型に合わせる |
Object is possibly 'null' |
nullの可能性がある値を直接使った |
nullチェック(if文)またはOptional Chaining(?.)を使う |
Object is possibly 'undefined' |
undefinedの可能性がある値を使った |
オプショナルチェーン(?.)やデフォルト値(??)を使う |
Property 'X' does not exist on type 'Y' |
型Yに存在しないプロパティXにアクセス |
型定義を確認、または型ガードで型を絞り込む |
Parameter 'x' implicitly has an 'any' type |
関数パラメータに型注釈がない |
パラメータに明示的な型注釈を追加する |
Argument of type 'X' is not assignable to parameter of type 'Y' |
関数の引数の型が一致しない |
引数の型を確認し、正しい型の値を渡す |
Type 'X' is not assignable to type 'never' |
switchの網羅性チェックに漏れがある |
未処理のcaseを追加する |
エラー対処の実例
Object is possibly ‘null’ の対処
// エラーになるコード
const el = document.getElementById("app");
el.textContent = "Hello"; // エラー: Object is possibly 'null'
// 対処法1: nullチェック
if (el) {
el.textContent = "Hello"; // OK
}
// 対処法2: Optional Chaining + Nullish Coalescing
const text = el?.textContent ?? "default";
// 対処法3: Non-null Assertion(確実に存在すると分かっている場合のみ)
const el2 = document.getElementById("app")!;
el2.textContent = "Hello"; // OK(ただし要素がない場合は実行時エラー)
型の絞り込み(Narrowing)
// Union型の値を安全に扱う
function processValue(value: string | number | boolean) {
// そのままではstringのメソッドは使えない
// value.toUpperCase(); // エラー
if (typeof value === "string") {
return value.toUpperCase(); // OK: string型に絞り込まれた
}
if (typeof value === "number") {
return value.toFixed(2); // OK: number型に絞り込まれた
}
return value; // boolean型
}
as const の活用
// 問題: オブジェクトのプロパティが広い型になる
const config = {
method: "GET", // string型と推論される
url: "/api/users"
};
// fetchの第2引数はmethod: "GET" | "POST" | ... を期待
// fetch(config.url, { method: config.method }); // エラー
// 解決: as const でリテラル型にする
const config2 = {
method: "GET",
url: "/api/users"
} as const;
// config2.method は "GET" 型(リテラル型)
// または個別にリテラル型を指定
const config3 = {
method: "GET" as "GET",
url: "/api/users"
};
まとめ
この記事では、TypeScriptの型の書き方を基本から応用まで体系的に解説しました。
記事の振り返り
- 基本型: string, number, boolean, null, undefined, any, unknown, void, neverの使い分け
- 配列・タプル:
number[] と [string, number] の違い
- オブジェクト型: interface(拡張・宣言マージ向き)と type(Union・プリミティブ型向き)の使い分け
- Union型・Intersection型:
| で「いずれか」、& で「すべてを満たす」型
- リテラル型: 特定の値に限定する型と、テンプレートリテラル型による文字列型の組み合わせ
- 型推論: TypeScriptが自動で型を判定する仕組み。冗長な型注釈を避けてコードを簡潔に
- 型アサーション:
as で型を明示。as const でリテラル型に固定
- 型ガード: typeof, instanceof, in, ユーザー定義型ガードで安全に型を絞り込む
- Generics: 型パラメータで再利用可能な型安全コード。extends制約で型を限定
- ユーティリティ型: Partial, Required, Pick, Omit, Record等で既存型を変換
- Mapped Types・Conditional Types: 型レベルのループと条件分岐
- 実務パターン: APIレスポンス型、イベントハンドラー型、AsyncState、Branded Types等
TypeScriptの型システムは非常に強力で、適切に活用することでバグの早期発見、コードの自己文書化、リファクタリングの安全性を大幅に向上できます。
最初はすべてを覚える必要はありません。まずは基本型とUnion型を使いこなすところから始め、徐々にGenericsやユーティリティ型に挑戦していきましょう。型エラーは「敵」ではなく、バグを未然に防いでくれる「味方」です。
ポイント:TypeScriptの公式ハンドブック(typescriptlang.org)は最も信頼できるリソースです。この記事で学んだ内容をさらに深めたい場合はぜひ参照してください。