4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】型まわりを整理する:実務でよく使うパターン11選

4
Posted at

概要

TypeScriptには「型」に関するキーワードが多く、混乱しがち...。
この記事では、実務で実際に登場したパターンをサンプルコードとともに整理します。

目次

  1. 型アノテーション
  2. 型アサーション
  3. 型ガード
  4. Discriminated Union(判別可能なUnion型)
  5. 特殊な型:never / unknown / any
  6. ジェネリクス
  7. Zod(スキーマバリデーション)
  8. DBスキーマ型(Kyselyパターン)
  9. DTO + デコレータ(NestJSパターン)
  10. Stream Event型
  11. 定数と型を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 型だと保証する」という意味です。呼び出し元に対してvalueDirection 型だと教えてあげる、ということですね。

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から来たデータは stringunknown 型です。
型ガードで「この値は安全」と確認してから使うことで、実行時エラーを防げます。


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

anyunknown はどちらもあらゆる値を代入できますが、使用時の安全性が異なります。

// 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の型システムは、コードが正しく動くことをコンピュータに証明させる仕組みと言えます。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?