概要
TypeScriptには「型」に関するキーワードが多く、混乱しがち...。
この記事では、実務で実際に登場したパターンをサンプルコードとともに整理します。
目次
- 型アノテーション
- 型アサーション
- 型ガード
- Discriminated Union(判別可能なUnion型)
- 特殊な型:never / unknown / any
- ジェネリクス
- Zod(スキーマバリデーション)
- DBスキーマ型(Kyselyパターン)
- DTO + デコレータ(NestJSパターン)
- Stream Event型
- 定数と型を1か所で管理する
1. 型アノテーション
変数・引数・戻り値に : 型名 を書いて型を明示する、最も基本的な書き方です。
// 変数
const name: string = 'Alice';
const age: number = 30;
const isActive: boolean = true;
// 関数の引数と戻り値
function greet(name: string): string {
return `Hello, ${name}`;
}
// 型が明らかな場合は省略できる(型推論)
const count = 42; // number と推論される
書かなくてもTypeScriptが推論してくれますが、引数には書くのが一般的です。
2. 型アサーション
「この値はこの型として扱ってください」とTypeScriptに伝える書き方です。as 型名 と書きます。
const input = document.getElementById('username'); // HTMLElement | null
const inputEl = input as HTMLInputElement; // HTMLInputElement として扱う
inputEl.value; // value プロパティにアクセスできる
注意点
型アサーションは「私が責任を持つ」という宣言です。
間違った型を指定してもコンパイルは通ってしまうため、乱用は避けるべき。
const name = 'Alice' as unknown as number; // 通るが実行時に壊れる
3. 型ガード
実行時に値の型を検証し、TypeScriptにその型を伝える関数です。
戻り値を value is 型名 と書くのがポイントです。
type Direction = 'north' | 'south' | 'east' | 'west';
// 型ガード関数
function isValidDirection(value: string): value is Direction {
return ['north', 'south', 'east', 'west'].includes(value as Direction);
// ↑ includes() の型合わせのため as を使用(型アサーション)
}
value is Direction という戻り値型は「true を返したとき、引数は Direction 型だと保証する」という意味です。呼び出し元に対してvalueは Direction 型だと教えてあげる、ということですね。
※includes()は「配列にその値が含まれているか」を true/false で返すJavaScriptの組み込みメソッド
const directionFromAPI: string = fetchDirectionFromAPI(); // API から来た string
if (isValidDirection(directionFromAPI)) {
// このブロック内では directionFromAPI が Direction に絞り込まれる
move(directionFromAPI); // OK
}
// ガードの外では string のまま
move(directionFromAPI); // コンパイルエラー:string は Direction を期待する関数に渡せない
なぜ必要?
DBやAPIから来たデータは string や unknown 型です。
型ガードで「この値は安全」と確認してから使うことで、実行時エラーを防げます。
4. Discriminated Union(判別可能なUnion型)
取りうる値の種類をコードで制限するパターンです。| で複数の型を結合します。
// リテラル型のUnion
type Shape = 'circle' | 'rectangle' | 'triangle';
// OK
const shape1: Shape = 'circle';
// コンパイルエラー(存在しない値)
const shape2: Shape = 'square';
各値に固有のフィールドを持たせると、if で絞り込んだブロック内でそのフィールドだけにアクセスできます。
type Figure =
| { shape: 'circle'; radius: number }
| { shape: 'rectangle'; width: number; height: number }
| { shape: 'triangle'; base: number; height: number };
function getArea(fig: Figure): number {
if (fig.shape === 'circle') {
return Math.PI * fig.radius ** 2; // circle のときだけ radius にアクセスできる
}
if (fig.shape === 'rectangle') {
return fig.width * fig.height; // rectangle のときだけ width / height にアクセスできる
}
return (fig.base * fig.height) / 2; // triangle のときだけ base にアクセスできる
}
5. 特殊な型:never / unknown / any
never
never は「到達しない」「存在しない」を型で表現します。
以下は絶対に return しない関数です。
type ErrorCode = 'NOT_FOUND' | 'UNAUTHORIZED';
function throwError(code: ErrorCode): never {
throw new Error(code);
// return しないことを型で表現
}
function getUser(id: number): User {
const user = db.find(id);
if (!user) throwError('NOT_FOUND'); // ← never なので後続は user が必ず存在すると判断される
return user; // TypeScript が user: User と認識できる(undefined チェック不要)
}
never を返す関数の呼び出し後は「そのコードには到達しない」とTypeScriptが判断するため、後続のコードで変数の型が確定します。
unknown vs any
any と unknown はどちらもあらゆる値を代入できますが、使用時の安全性が異なります。
// any:型チェックを完全に無効化
let x: any = 'hello';
x.foo.bar.baz; // エラーにならない → 実行時に壊れる
// unknown:型チェックを維持(安全)
let y: unknown = 'hello';
y.toUpperCase(); // コンパイルエラー → 型を確認してから使う必要がある
if (typeof y === 'string') {
y.toUpperCase(); // OK
}
外部データを受け取るときは Record<string, unknown> の形がよく使われます。
キーは文字列、値は不明という意味です。
function handleApiResponse(data: Record<string, unknown>) {
if (typeof data.name === 'string') {
console.log(data.name.toUpperCase()); // 型を確認してから使う
}
}
6. ジェネリクス
「型を引数として受け取る」機能です。<T> のように書き、呼び出し側が具体的な型を決めます。
// T は「型の引数」
function identity<T>(value: T): T {
return value;
}
identity(42); // T = number と推論される。戻り値も number
identity('hello'); // T = string と推論される。戻り値も string
実務ではAPIのレスポンスを扱う関数によく使います。
async function fetchData<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as T;
}
// 呼び出し側が型を決める
const user = await fetchData<{ id: number; name: string }>('/api/user');
user.name; // string として補完される
user.foo; // コンパイルエラー
Promise<T>は「非同期処理が完了したとき、T 型の値が返ってくる」という意味です。
Tは「awaitした後の値の型が何になるか」をTypeScriptに伝えるためにあります。
7. Zod(スキーマバリデーション)
import { z } from 'zod';
// スキーマを定義
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// スキーマから TypeScript の型を自動生成(重複定義なし)
type User = z.infer<typeof UserSchema>;
// ↑ { id: number; name: string; email: string } と同じ
※ z.infer はスキーマが持っている型情報を取り出して名前(User)をつけるだけです。
// APIレスポンスを実行時に検証
const response = await fetch('/api/user').then(r => r.json());
const user = UserSchema.parse(response); // 不正な値なら例外を投げる
user.name; // string として補完される
ジェネリクスと組み合わせる
z.ZodType<T> は「どんなZodスキーマでも受け取れる」という親の型です。
渡したスキーマから T が自動で決まる仕組みになっています。
async function fetchAndValidate<T>(
url: string,
schema: z.ZodType<T>, // 渡したスキーマから T が決まる
): Promise<T> {
const response = await fetch(url).then(r => r.json());
return schema.parse(response);
// ・形式が不正なら例外を投げる
// ・検証OKなら T 型(= User など)として返す
}
// 呼び出し側
const user = await fetchAndValidate('/api/user', UserSchema);
// UserSchema から T = User と推論されるので user は User 型になる
user.name; // string として使える
※ TypeScript が T = User と推論してくれるので、明示的に書かなくてもOK
// 書いても動くが不要
const user = await fetchAndValidate<User>('/api/user', UserSchema);
Zodが実行時のバリデーション、TypeScriptがコンパイル時のチェックという二重の守りになります。
8. DBスキーマ型(Kyselyパターン)
Kysely は型安全なSQLクエリビルダーです。
テーブルの型をTypeScriptのインターフェースで定義します。
import { ColumnType, Generated } from 'kysely';
interface ProductsTable {
id: Generated<number>; // 自動採番(INSERT時は省略可)
name: string;
price: number;
created_at: ColumnType<Date, string | undefined, never>;
// ↑読み取り ↑挿入 ↑更新(never = 更新不可)
}
interface Database {
products: ProductsTable;
}
ColumnType<読み取り型, 挿入型, 更新型> で操作ごとに型を分けられます。
更新型を never にすることで「このカラムは更新禁止」というDBのルールをコードで表現できます。
// SELECT: Date として取得できる
const product = await db.selectFrom('products').select('created_at').executeTakeFirst();
product?.created_at; // Date 型
// INSERT: 文字列で渡せる
await db.insertInto('products').values({ name: 'Widget', price: 980 }).execute();
// UPDATE: created_at は never なのでコンパイルエラー
await db.updateTable('products').set({ created_at: '2024-01-01' }).execute();
9. DTO + デコレータ(NestJSパターン)
NestJS では DTO(Data Transfer Object) クラスにデコレータを付けて、
バリデーション・型・APIドキュメントを1か所で管理します。
import { IsString, IsEmail, IsOptional, IsInt, Min, Max } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: 'ユーザー名', example: 'Alice' })
@IsString()
name!: string;
@ApiProperty({ description: 'メールアドレス', example: 'alice@example.com' })
@IsEmail()
email!: string;
@ApiPropertyOptional({ description: '年齢', example: 25 })
@IsOptional()
@IsInt()
@Min(0)
@Max(120)
age?: number;
}
1つのクラスが3つの役割を担います。
| デコレータ | 役割 |
|---|---|
@IsString / @IsEmail / @IsInt
|
リクエストの実行時バリデーション |
@ApiProperty |
Swaggerドキュメントの自動生成 |
TypeScript型(string, number) |
コード内の型チェック・補完 |
なぜ1か所にまとめるか
型・バリデーション・ドキュメントを別々に定義すると「型は直したけどバリデーションを直し忘れた」という変更漏れが起きます。
DTOクラスに集約することで、1か所変えれば全てに反映されます。
// バラバラに定義した場合(変更漏れが起きやすい)
type CreateUserRequest = { name: string; email: string; age?: number }; // 型定義
const schema = z.object({ name: z.string(), email: z.string() }); // バリデーション(age を追加し忘れ)
// @ApiProperty(...) // ドキュメント(別ファイルに散在)
// DTOにまとめた場合(1か所を変えれば全て更新される)
export class CreateUserDto { /* 型・バリデーション・ドキュメントが同じクラスに */ }
10. Stream Event型
SSE(Server-Sent Events)などのストリーミングでは、
送受信するイベントの種類をUnion型で制限するパターンがよく使われます。
// 送信できるイベントの種類を型で縛る
type TaskEventType =
| 'task.progress' // 進捗通知
| 'task.done' // 完了通知
| 'task.error'; // エラー通知
interface TaskEvent {
type: TaskEventType;
[key: string]: unknown;
}
// ファクトリ関数で型を固定
function createProgressEvent(percent: number): TaskEvent {
return { type: 'task.progress', percent };
}
function createDoneEvent(result: unknown): TaskEvent {
return { type: 'task.done', result };
}
// typoしてもコンパイルエラーにならない(型なし)
sendEvent({ type: 'task.progres' }); // typoに気づけない
// 型があればコンパイルエラーで即検出
const eventType: TaskEventType = 'task.progres'; // コンパイルエラー!
フロントエンドとバックエンドの契約をコードで表現でき、
どちらかが変更されたとき型エラーとして気づけます。
11. 定数と型を1か所で管理する
as const(const アサーション)
配列やオブジェクトに as const を付けると、値がリテラル型として固定されます。
// as const なし → 型は string[](どんな文字列でも入れられる)
const SIZES = ['small', 'medium', 'large'];
// as const あり → 型は readonly ['small', 'medium', 'large'](値が固定される)
const SIZES = ['small', 'medium', 'large'] as const;
(typeof 配列)[number](インデックスアクセス型)
as const の配列からUnion型を自動生成するパターンです。
export const SIZES = ['small', 'medium', 'large'] as const;
// 配列の要素型を自動生成
export type Size = (typeof SIZES)[number];
// → 'small' | 'medium' | 'large'
配列とUnion型を1か所で管理できます。配列に値を追加するだけで型にも自動で反映されます。
const SIZES = ['small', 'medium', 'large', 'x-large'] as const;
type Size = (typeof SIZES)[number];
// → 'small' | 'medium' | 'large' | 'x-large'(追加するだけで型も更新)
keyof typeof
オブジェクトのキーから型を生成します。
const HTTP_ERRORS = {
NOT_FOUND: 'NOT_FOUND',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
} as const;
type HttpErrorCode = keyof typeof HTTP_ERRORS;
// → 'NOT_FOUND' | 'UNAUTHORIZED' | 'FORBIDDEN'
定数オブジェクトとその型を1か所にまとめられます。キーを追加・削除するだけで型も追従します。
まとめ
| パターン | 何を防ぐか |
|---|---|
| 型アノテーション | 引数や変数への誤った型の代入 |
| 型アサーション | 型の不一致によるコンパイルエラー(乱用注意) |
| 型ガード | 外部データの型検証漏れ |
| Discriminated Union | 存在しない値の使用・typo |
| never / unknown / any | 到達不能コードの型漏れ・unsafe な型操作 |
| ジェネリクス | 同じロジックを型ごとに重複定義すること |
| Zod | 実行時のデータ不整合・型定義の重複 |
| DBスキーマ型 | 更新不可カラムへの書き込みなどのDBルール違反 |
| DTO + デコレータ | バリデーション・型・ドキュメントの変更漏れ |
| Stream Event型 | イベント種別のtypoや存在しないイベントの送信 |
| 定数と型の自動生成 | 配列・オブジェクトと型定義の二重管理・変更漏れ |
共通のテーマは「バグを実行前(コンパイル時)に発見する」 ですね。
TypeScriptの型システムは、コードが正しく動くことをコンピュータに証明させる仕組みと言えます。