Skip to content

TypeScript チートシート

スケールする JavaScript の型付きスーパーセット。

01

はじめに

Hello World とコンパイル

TypeScript ファイルは .ts 拡張子を使用します。tsc コンパイラが TS を JS にトランスパイルし、実行時にすべての型注釈を消去します。最大の型安全性には --strict を使用してください。ts-node や bun は別のコンパイルステップなしで .ts ファイルを直接実行できます。

typescript
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);

// Compile to JavaScript (type annotations erased):
//   tsc hello.ts          -> hello.js
//   tsc --strict hello.ts  // enable all strict checks
//   tsc --watch hello.ts   // recompile on change

// Run directly with ts-node or bun:
//   ts-node hello.ts

tsconfig.json

tsconfig.json は TypeScript コンパイラを設定します。'strict: true' は noImplicitAny、strictNullChecks、strictFunctionTypes などを有効にします。'target' は出力 JS バージョンを制御します。'esModuleInterop' は Node の組み込みのような CommonJS モジュールからのデフォルトインポートを有効にします。

typescript
{
  "compilerOptions": {
    "target": "ES2020",        // JS version to emit
    "module": "ESNext",        // module system
    "strict": true,            // enable all strict checks
    "outDir": "./dist",        // output directory
    "rootDir": "./src",        // source root
    "esModuleInterop": true,   // allow default imports from CJS
    "skipLibCheck": true,      // skip .d.ts type checking
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

型注釈と推論

型注釈は変数の型を明示的に指定します。TypeScript は値から型を推論することもできます。関数シグネチャと公開 API には明示的な注釈を使用し、ローカル変数には推論に頼ってください。'any' は避けてください — 型チェックから完全に脱退します。

typescript
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];

// Type inference (let TS figure it out)
let age = 30;           // inferred as number
let items = [1, 2, 3];  // inferred as number[]
let mixed = [1, "a"];   // inferred as (string | number)[]

// any disables type checking (avoid!)
let anything: any = 42;
anything = "string";    // no error

厳格モードのチェック

厳格モードは重要なチェックを有効にします:strictNullChecks(null/undefined は他の型に代入不可)、noImplicitAny(パラメータには型が必要)、strictPropertyInitialization(クラスフィールドは初期化が必要)。フィールドが後で設定されることが確実な場合は '!'(確定代入)を使用してください。

typescript
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode

// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any

// strictPropertyInitialization
class User {
  name: string; // Error: not initialized
  constructor() {}
}

// Fix: initialize or use definite assignment
class Fixed {
  name!: string; // definite assignment assertion
  age: number = 0;
}

宣言ファイル(.d.ts)

宣言ファイル(.d.ts)は TypeScript 定義のない JavaScript ライブラリに型を提供します。'declare' はコンパイラに変数/関数が実行時に存在することを伝えます。人気ライブラリには DefinitelyTyped の @types パッケージを使用してください(例:@types/node、@types/react)。

typescript
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
  export function greet(name: string): string;
  export const version: string;
}

// global.d.ts - extend global scope
declare global {
  interface Window {
    myApp: { version: string };
  }
}

// Usage in .ts files:
// import { greet } from "my-lib";  // now typed
// Install @types packages: npm i -D @types/node @types/react
02

基本型

プリミティブと特殊型

TypeScript プリミティブ:string、number、boolean、bigint、symbol。'void' は関数が値を返さないことを示します。'never' は決して発生しない値を表します — 例外をスローするか永遠に実行する関数。switch 文の網羅的チェックに 'never' を使用してください。

typescript
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");

// void: function returns nothing
function log(msg: string): void { console.log(msg); }

// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }

配列とタプル

配列は T[] または Array<T> 構文を使用します。ReadonlyArray は変更を防ぎます。タプルは各インデックスに特定の型を持つ固定長配列です — キーと値のペアや CSV のようなデータに有用です。ラベル付きタプルは名前付き位置で可読性を向上させます。

typescript
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable

// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1];  // number

// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];

列挙型

列挙型は名前付き定数のセットを定義します。デバッグには文字列列挙が推奨されます(値が出力で読みやすい)。数値列挙は逆マッピングをサポートします。'const enum' はコンパイル時に消去され(インライン化)、ランタイムコストゼロです。単純なケースにはユニオン型を優先してください。

typescript
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up;  // 0

// String enum (recommended for readability)
enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"

// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

any vs unknown vs never

'any' は全ての型チェックを無効にします — 避けてください。'unknown' は型安全な代替です:使用前に(typeof、instanceof で)ナローイングする必要があります。'never' は決して発生しない値を表し、switch 文の網羅性チェックで欠落ケースをコンパイル時にキャッチするために使用されます。

typescript
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string";       // OK
a.toUpperCase();    // OK (no check, may fail at runtime)

// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
  u.toUpperCase();  // OK after narrowing
}

// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return Math.PI;
    case "square": return 1;
    default:
      const _exhaustive: never = s; // Error if case missing
  }
}

型アサーション

型アサーションはコンパイラに「信じて、型を知っている」と伝えます。'as' 構文を使用します。非 null アサーション(!)は値が null/undefined でないことを TS に伝えます。'as const' は全プロパティを読み取り専用リテラルにします — 設定オブジェクトや Redux アクション型に有用です。アサーションはランタイム動作を変更しません。

typescript
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;

// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi";  // el is HTMLElement, not null

// Double assertion (for unsafe casts)
let value = "42" as unknown as number;

// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonly

リテラル型とユニオン型

リテラル型は値を特定の文字列、数値、ブール値に制限します。ユニオンと組み合わせて、direction や HTTP メソッドのような精密な型を作成します。テンプレートリテラル型(TS 4.1+)は他の型から文字列型を構築します — 型安全なキーとイベント名の生成に強力です。

typescript
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left";  // OK
// direction = "sideways"; // Error

// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4;  // OK

// Boolean literal
let flag: true = true;

// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
03

インターフェースとオブジェクト

インターフェースの基礎

インターフェースはオブジェクトの形状を記述します。'?' はオプショナルプロパティ(undefined の可能性あり)をマークします。'readonly' は初期化後の再代入を防ぎます。インターフェースはコンパイル時のみ — 出力 JavaScript では消去されます。オブジェクトとクラスの契約を定義するために使用します。

typescript
interface User {
  id: number;
  name: string;
  email?: string;        // optional property
  readonly createdAt: Date; // immutable
}

const user: User = {
  id: 1,
  name: "Alice",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefined

インデックスシグネチャ

インデックスシグネチャは指定した型の任意のキーを持つオブジェクトを許可します。全プロパティ値はインデックス型に代入可能でなければなりません。辞書、キャッシュ、動的データに有用です。既知のプロパティと組み合わせて、追加オプション付きの型付き設定に使用します。

typescript
// Index signature: arbitrary string keys
interface StringMap {
  [key: string]: string;
}

const dict: StringMap = {
  name: "Alice",
  city: "NYC",
  // count: 42, // Error: value must be string
};

// Mixed: known + index signature
interface Config {
  name: string;
  [key: string]: string | number;
}

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}

インターフェースの拡張

インターフェースは1つ以上の他のインターフェースを拡張でき、メンバーを組み合わせます。これにより合成とコード再利用が可能になります。クラスとは異なり、インターフェースは多重継承をサポートします。インターフェースを実装する場合、クラスは全必須メンバーを提供する必要があります。

typescript
interface Animal {
  name: string;
  eat(): void;
}

interface Pet extends Animal {
  owner: string;
  play(): void;
}

interface Swimmer {
  swim(): void;
}

// Multiple inheritance
interface Duck extends Pet, Swimmer {
  quack(): void;
}

const duck: Duck = {
  name: "Donald",
  owner: "Walt",
  eat() {},
  play() {},
  swim() {},
  quack() {},
};

インターフェース内の関数型

インターフェースは関数シグネチャを記述でき、型安全なコールバックを可能にします。ハイブリッドインターフェース(呼び出し可能 + プロパティ)はメソッドも持つ jQuery スタイルの関数に使用されます。このパターンは添付ヘルパーを持つ関数を返すライブラリで一般的です。

typescript
interface SearchFn {
  (source: string, keyword: string): boolean;
}

const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true

// Interface with mixed members (hybrid)
interface Counter {
  (start: number): void;  // callable
  count: number;           // property
  reset(): void;           // method
}

// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };

インターフェース vs 型エイリアス

インターフェースは宣言マージ(同名インターフェースの結合)、より良いエラーメッセージをサポートし、オブジェクト/クラス形状に推奨されます。型エイリアスはより柔軟(ユニオン、プリミティブ、タプルを表現可能)ですがマージできません。拡張可能な API にはインターフェースを、ユニオンと計算型には型エイリアスを使用してください。

typescript
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };

// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;

// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };

// Use interface for objects/classes, type for unions/aliases

オプショナルチェーンとヌリッシュ合体

オプショナルチェーン(?.)はネストされたプロパティに安全にアクセスします — リンクのいずれかが null/undefined の場合、例外ではなく undefined を返します。ヌリッシュ合体(??)は null/undefined のみにデフォルトを提供します(0 や '' には提供しない)。これらの演算子は冗長な null チェックコードを大幅に削減します。

typescript
interface User {
  profile?: {
    address?: {
      city?: string;
    };
  };
}

const user: User = {};

// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined

// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";

// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky

// Optional method call
const result = user.profile?.address?.city?.toUpperCase();
04

型エイリアスとユニオン

型エイリアス

型エイリアスはユニオン、交差、プリミティブ、ジェネリクスを含む任意の型への名前付き参照を作成します。インターフェースとは異なり、エイリアスはマージや拡張できませんが、より柔軟です。ユニオン、タプル、ユーティリティ型にはエイリアスを、オブジェクト形状にはインターフェースを使用してください。

typescript
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };

// Generic alias
type Container<T> = { value: T };

// Function type alias
type Handler<T> = (event: T) => void;

// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);

ユニオン型

ユニオン型(A | B)は値がいくつかの型のいずれかになることを許可します。TypeScript は typeof、instanceof、in チェックを使用して条件ブロック内で型をナローイングします。注意:(string | number)[] は string[] | number[] とは異なります — 前者は混合配列、後者は全て文字列または全て数値です。

typescript
// Union: value can be one of several types
type ID = string | number;

function display(id: ID) {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // narrowed to string
  } else {
    console.log(id.toFixed(2));    // narrowed to number
  }
}

display("abc");  // ABC
display(42);     // 42.00

// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];

交差型

交差型(A & B)は複数の型の全メンバーを組み合わせます — 結果は全ての型を満たす必要があります。ミックスイン、合成、ユーティリティ型のマージに有用です。ユニオン(OR)とは異なり、交差は AND です:値は全ての型の全プロパティを持つ必要があります。

typescript
// Intersection: combine multiple types into one
interface BusinessPartner {
  name: string;
  credit: number;
}

interface Identity {
  id: number;
  email: string;
}

type Employee = BusinessPartner & Identity;

const emp: Employee = {
  name: "Alice",
  credit: 1000,
  id: 1,
  email: "[email protected]",
};

// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing props

Nullable 型

厳格モードでは、null と undefined は他の型に代入できません — ユニオン(string | null)で明示的に含める必要があります。オプショナルパラメータ(param?)は暗黙的に T | undefined です。安全なデフォルトには ?? を、非 null アサートには ! を使用してください(控えめに使用)。

typescript
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice";  // OK

// Optional parameters are implicitly | undefined
function greet(name?: string) {
  // name is string | undefined
  console.log(name ?? "Guest");
}

// Return type can be null
function find(id: number): string | null {
  return id === 1 ? "Alice" : null;
}

// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"

Keyof と Typeof 演算子

'keyof T' は型 T のキーを文字列リテラルユニオンとして抽出します。'typeof x' は値の型を抽出します(オブジェクトから推論するのに有用)。'keyof typeof obj' は両者を組み合わせて既存オブジェクトのキーを取得します — Redux アクション型や型安全なプロパティアクセサで一般的です。

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"

function getProp(obj: User, key: keyof User) {
  return obj[key];
}

// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }

// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"

マップ型

マップ型はキーを反復して型を変換します。Readonly、Partial、Pick のような組み込みユーティリティはマップ型です。+ と - 修飾子で readonly やオプショナルを追加/削除します。キーリマッピング(TS 4.1+)はテンプレートリテラル型を使用してキー名を変更します — getter/setter 型の生成に強力です。

typescript
// Map over keys to create a new type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface User { id: number; name: string; }

type ReadonlyUser = Readonly<User>;   // all readonly
type OptionalUser = Optional<User>;   // all optional

// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };

// Key remapping (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
05

関数

関数型とシグネチャ

TypeScript は関数パラメータと戻り値に型注釈を追加します。戻り値の型は推論できることが多いですが、公開 API には明示的な注釈が推奨されます。デフォルトパラメータは引数をフォールバック値付きのオプショナルにします。関数が何も返さない場合は void を使用してください。

typescript
// Named function with types
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with types
const multiply = (a: number, b: number): number => a * b;

// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;

// Void return (no return value)
function log(msg: string): void { console.log(msg); }

// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
  return `${greeting}, ${name}!`;
}
greet("Alice");        // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"

残余パラメータとタプル

残余パラメータ(...args)は複数の引数を配列に集めます。TypeScript は T[] または固定長可変引数関数用のタプルとして型付けします。スプレッド演算子(...)は逆を行います — 配列を個々の引数に展開します。タプル残余型は精密な可変長シグネチャを可能にします。

typescript
// Rest parameters (variadic)
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
  console.log(name, scores);
}

// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));

// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
  const [s, n, b] = args;
}

関数のオーバーロード

関数オーバーロードは同じ関数に複数の型シグネチャを提供し、入力に基づく精密な戻り値型を可能にします。実装シグネチャは呼び出し元から隠されます。オーバーロードは上から下に解決されます — より具体的なシグネチャを先に置いてください。jQuery や lodash のようなライブラリで一般的です。

typescript
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return [input, input * 2];
}

const strs = parse("a,b,c"); // string[]
const nums = parse(42);      // number[]

// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
  return m === undefined
    ? new Date(yOrTs)
    : new Date(yOrTs, m - 1, d);
}

this 型

TypeScript では最初のパラメータとして 'this' 型を宣言できます。これにより関数が正しいコンテキストで呼び出されることが保証されます — コールバックとして渡されるメソッドに有用です。アロー関数は 'this' をレキシカルにキャプチャし、.bind() の必要性を回避します。型付けされていない 'this' エラーをキャッチするには 'noImplicitThis' を使用してください。

typescript
interface Card {
  suit: string;
  rank: string;
  isFaceUp(): boolean;
}

// Explicit 'this' parameter
function format(this: Card): string {
  return `${this.rank} of ${this.suit}`;
}

const card: Card = {
  suit: "Hearts",
  rank: "A",
  isFaceUp() { return true; },
  format,
};

// 'this' in callbacks with bind
class Handler {
  private count = 0;
  increment = () => { this.count++; }; // arrow binds this
}

コールバックと高階関数

TypeScript は高階関数(関数を取るまたは返す関数)を完全に型付けします。入力と出力間の型関係を保持するためジェネリック型パラメータ(T、U)を使用してください。コールバック型は再利用のため型エイリアスとして定義されることが多いです。カリー化(関数を返す)は完全に型安全です。

typescript
// Function as parameter
type Callback<T> = (value: T, index: number) => void;

function forEach<T>(arr: T[], cb: Callback<T>): void {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
  }
}

forEach(["a", "b"], (v, i) => console.log(i, v));

// Function returning function (curry)
function add(a: number): (b: number) => number {
  return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8

// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
  return arr.map(fn);
}

パラメータの分割代入

TypeScript は関数パラメータでの分割代入をサポートします — 分割された形状をインラインまたはインターフェース経由で注釈します。インターフェースへの抽出は可読性と再利用を向上させます。配列/タプルの分割代入も機能します。このパターンは React コンポーネントの props と API ハンドラで一般的です。

typescript
// Destructured parameters with types
function createUser({ name, age, email }: {
  name: string;
  age: number;
  email?: string;
}): void {
  console.log(name, age, email);
}

createUser({ name: "Alice", age: 30 });

// Extract to interface for reuse
interface UserOpts {
  name: string;
  age: number;
  email?: string;
}

function updateUser({ name, age }: UserOpts): void {}

// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
  return [b, a];
}
06

クラスと OOP

クラスとコンストラクタ

TypeScript クラスはパラメータプロパティをサポートします — コンストラクタパラメータにアクセス修飾子(public/private/protected/readonly)を接頭辞として付けると、フィールドの自動作成と代入を行います。この省略記法はボイラープレートを削減します。メソッドは戻り値に型注釈を持てます。フィールドはデフォルトで public です。

typescript
class Person {
  // Parameter properties (shorthand)
  constructor(
    public name: string,    // auto-creates this.name
    private age: number,    // private field
    readonly id: number,    // immutable
  ) {}

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

const p = new Person("Alice", 30, 1);
console.log(p.name);  // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonly

アクセス修飾子

アクセス修飾子:public(デフォルト、どこでも)、private(クラスのみ)、protected(クラス + サブクラス)、readonly(不変)。TypeScript の 'private' はコンパイル時のみ。ES '#' プライベートフィールドは実行時に真にプライベートです。実装の詳細には private を、拡張ポイントには protected を使用してください。

typescript
class BankAccount {
  public owner: string;       // accessible everywhere
  private balance: number;    // class only
  protected rate: number;     // class + subclasses
  readonly id: string;        // immutable after init
  #secret: string;            // ES private (runtime)

  constructor(owner: string) {
    this.owner = owner;
    this.balance = 0;
    this.rate = 0.05;
    this.id = crypto.randomUUID();
    this.#secret = "hidden";
  }

  deposit(amount: number): void {
    this.balance += amount;
  }
}

継承と抽象クラス

抽象クラスは直接インスタンス化できません — サブクラスの基底を定義します。抽象メソッドは基底クラスに実装がなく、サブクラスが実装する必要があります。継承には 'extends' を、親コンストラクタの呼び出しには 'super()' を使用してください。抽象クラスはポリモーフィズムを可能にします — コードは任意の Shape サブクラスで動作します。

typescript
abstract class Shape {
  constructor(public color: string) {}
  abstract area(): number;  // must be implemented
  describe(): string {
    return `${this.color} shape, area ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(color: string, private r: number) {
    super(color);
  }
  area(): number { return Math.PI * this.r ** 2; }
}

class Square extends Shape {
  constructor(color: string, private side: number) {
    super(color);
  }
  area(): number { return this.side ** 2; }
}

const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstract

インターフェースと implements

クラスは複数のインターフェースを実装できます(カンマ区切り)。クラスは全インターフェースメンバーを提供する必要があります。extends(単一継承)とは異なり、implements は複数の契約をサポートします。これが TypeScript の多重継承的な振る舞いを実現する方法です。契約の定義にはインターフェースを、実装にはクラスを使用してください。

typescript
interface Printable {
  toString(): string;
}

interface Comparable<T> {
  compareTo(other: T): number;
}

class Money implements Printable, Comparable<Money> {
  constructor(private amount: number) {}

  toString(): string {
    return `$${this.amount.toFixed(2)}`;
  }

  compareTo(other: Money): number {
    return this.amount - other.amount;
  }
}

const a = new Money(10);
const b = new Money(20);
console.log(a.toString());      // "$10.00"
console.log(a.compareTo(b));    // -10

ゲッターとセッター

ゲッターとセッターはバリデーション、計算、副作用のためにプロパティアクセスを傍受します。プライベートバッキングフィールドを使用します(慣習:アンダースコア接頭辞)。ゲッターは計算プロパティ(摂氏から華氏など)を可能にします。セッターはバリデーションを可能にします。通常のプロパティのようにアクセスします — 括弧は不要です。

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) throw new Error("Below absolute zero");
    this._celsius = value;
  }

  get fahrenheit(): number {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5 / 9;
  }
}

const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throws

静的メンバーとシングルトン

静的メンバーはインスタンスではなくクラスに属します — ClassName.member でアクセスします。定数、ユーティリティ関数、ファクトリメソッドに static を使用します。プライベートコンストラクタ + static getInstance() でシングルトンパターンを実装します。'as const' は静的配列をリテラル型付き読み取り専用にします。

typescript
class Logger {
  static instance: Logger;
  private logs: string[] = [];

  private constructor() {} // prevent direct new

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;

  log(msg: string): void {
    this.logs.push(msg);
  }
}

const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructor
07

ジェネリクス

ジェネリック関数

ジェネリクス(<T>)により型安全性を保持しながら任意の型で動作する関数を書けます。型パラメータ T は呼び出し時に埋められるプレースホルダーです — 明示的(identity<number>)または引数から推論。ジェネリクスにより再利用可能で型安全なデータ構造とアルゴリズムが可能になります。

typescript
// Generic function: preserves type relationship
function identity<T>(value: T): T {
  return value;
}

const n = identity<number>(42);    // T = number, returns number
const s = identity("hi");          // T inferred as string

// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

const p = pair("Alice", 30); // [string, number]

// Generic with array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

ジェネリッククラス

ジェネリッククラス(<T>)は型安全なコンテナを作成します。各インスタンスは特定の型に固定されます — Stack<number> は数値のみを受け入れます。これによりランタイムオーバーヘッドなしでコンパイル時に型エラーをキャッチします(ジェネリクスは消去されます)。コレクション(Stack、Queue、Map)やリアクティブラッパー(Observable<T>)で一般的です。

typescript
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];
  }

  get size(): number {
    return this.items.length;
  }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2

const strStack = new Stack<string>();
strStack.push("hello");

ジェネリック制約

制約(T extends SomeType)はジェネリックが受け入れる型を制限します。'T extends HasLength' は T が 'length' プロパティを持つことを保証します。'K extends keyof T'(keyof 制約)はキーがオブジェクトに存在することを保証し、正しい値の型を返します。制約によりジェネリクスでの型安全なプロパティアクセスとメソッド呼び出しが可能になります。

typescript
// Constraint: T must have a 'length' property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(item.length);
}

logLength("hello");     // 5 (string has length)
logLength([1, 2, 3]);   // 3 (array has length)
// logLength(42);        // Error: number has no length

// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a key

デフォルト型パラメータ

デフォルト型パラメータは指定されない場合のフォールバック型を提供します。一般的なケースのある API に有用です(例:ApiResponse のデフォルトは string)。デフォルトは先行パラメータに依存できます。制約と組み合わせて(T extends X = DefaultType)型安全なオプショナルジェネリクスを使用できます。

typescript
// Default type parameter
interface ApiResponse<T = string> {
  data: T;
  status: number;
}

const r1: ApiResponse = { data: "hello", status: 200 };       // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 };    // T = number

// Multiple defaults
class Container<T = string, U = number> {
  constructor(public a: T, public b: U) {}
}

const c1 = new Container("hi", 42);      // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>

// Default with constraint
interface Box<T extends object = { id: number }> {
  value: T;
}

ジェネリックインターフェースと型

ジェネリックインターフェースと型エイリアスは再利用可能で型安全な契約を作成します。Repository<T> は一貫した API でデータアクセスを抽象化します。Result<T, E> は例外なしのエラーハンドリング用の判別ユニオンです。デフォルト型パラメータ(E = Error)は一般的なケースのボイラープレートを削減します。

typescript
// Generic interface
interface Repository<T> {
  findById(id: string): Promise<T>;
  save(item: T): Promise<void>;
  delete(id: string): Promise<void>;
}

// Implement with concrete type
class UserRepo implements Repository<User> {
  async findById(id: string): Promise<User> { /* ... */ }
  async save(user: User): Promise<void> { /* ... */ }
  async delete(id: string): Promise<void> { /* ... */ }
}

// Generic type alias with conditional
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };

条件型

条件型(T extends U ? X : Y)は型レベルの if 文です。'infer R' は別の型内から型を抽出します(例:関数の戻り値型)。条件型はユニオンに分配されます — ToArray<string | number> は string[] | number[] になります。Exclude、Extract、NonNullable のような組み込みユーティリティがこれを使用します。

typescript
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">;   // true
type B = IsString<42>;     // false

// Extract return type
type AsyncReturnType<T> =
  T extends (...args: any[]) => Promise<infer R> ? R : never;

async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;
08

高度な型

ユーティリティ型

TypeScript は一般的な変換用の組み込みユーティリティ型を提供します:Partial(全オプショナル)、Pick(キー選択)、Omit(キー除外)、Record(キーと値のマップ)、Required(オプショナル削除)、ReturnType(関数の戻り値)、Parameters(関数パラメータをタプルとして)。これらにより反復的な型定義が不要になります。

typescript
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };

// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;

// Record: key-value map
type UserMap = Record<string, User>;

// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;

// ReturnType: function return type
type R = ReturnType<() => string>; // string

// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]

テンプレートリテラル型

テンプレートリテラル型(TS 4.1+)は他の型を補間して文字列型を構築します。ユニオンと組み合わせて、文字列のデカルト積を生成します。Capitalize/Uppercase がケースを変換します。型安全なイベント名、getter/setter 生成、API ルート型付けに使用します。マップ型の 'as' 句でキーリネームが可能です。

typescript
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">;    // "Foo"

// Getter names from keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"

infer キーワード

'infer' キーワードは条件型の 'extends' 句内で型変数を宣言し、再利用のために型をキャプチャします。ReturnType、Parameters、Awaited のようなユーティリティ型の基盤です。複雑な構造(配列、Promise、関数)から型を抽出するのに infer を使用し、手動で分解することなく抽出します。

typescript
// Extract return type of a function
type MyReturnType<T> =
  T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;    // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean

// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>;  // string

// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number

// Extract first parameter
type FirstParam<T> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // string

判別ユニオン

判別ユニオン(タグ付きユニオン)は共有リテラルフィールド('type'、'kind'、'tag')を使用してバリアントを区別します。TypeScript は各 switch ケースで型をナローイングし、ケース固有のフィールドにアクセスできます。'never' デフォルトで網羅性チェックが可能 — 新しいケースを追加すると、処理するまでコンパイラがエラーになります。Redux リデューサーとステートマシンに不可欠です。

typescript
// Discriminated union: shared 'type' (or 'kind') field
type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "DELETE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text }];
    case "DELETE_TODO":
      return state.filter(t => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      const _: never = action; // exhaustiveness check
      return state;
  }
}

型ガード:typeof と instanceof

型ガードはランタイムで型をナローイングします。'typeof' はプリミティブに機能します(string、number、boolean、symbol、bigint、undefined、function、object)。'instanceof' はクラス/コンストラクタのプロトタイプをチェックします。Array.isArray() は型付き配列にナローイングします。これらは組み込みガードです — カスタムコードは不要です。TypeScript は各ブランチでナローイングされた型を追跡します。

typescript
// typeof: narrow primitives
function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // string
  }
  return value.toFixed(2);      // number
}

// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }

function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // Dog
  } else {
    pet.meow(); // Cat
  }
}

// Array.isArray
function flatten(arr: (number | number[])[]) {
  return arr.flatMap(x =>
    Array.isArray(x) ? x : [x]
  );
}

カスタム型ガードと in 演算子

'in' 演算子はプロパティがオブジェクトに存在するかチェックし、それを持つ型にナローイングします。型述語(x is T)はブール値を返すが型もナローイングするカスタムガード関数です。外部データの安全な解析(JSON.parse、API レスポンス)には 'unknown' を入力型として使用します。述語により再利用可能で合成可能な型チェックが可能になります。

typescript
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly(); // Bird
  }
}

// Type predicate: custom guard function
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: any): x is { name: string; age: number } {
  return x && typeof x.name === "string" && typeof x.age === "number";
}

const val: unknown = "hello";
if (isString(val)) {
  console.log(val.toUpperCase()); // narrowed to string
}
09

データ構造

配列と ReadonlyArray

TypeScript 配列は型付きです — map、filter、reduce のようなメソッドは要素型を保持します。不変性には readonly T[] または ReadonlyArray<T> を使用します。タプルは固定長と型付き位置を持ちます。配列の分割代入とスプレッドは完全に型安全です。型システムがインデックス範囲外や誤った型の代入をキャッチします。

typescript
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];

// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly

// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];

// Array methods are typed
const doubled = nums.map(n => n * 2);    // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number

// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];

オブジェクトと Record

Record<K, V> は型 K のキーと型 V の値を持つ型付きマップを作成します。Partial<T> は全プロパティをオプショナルにします — 更新/パッチ操作に理想です。Object.entries/keys/values は型付き配列を返します。既存の型から焦点を絞った型を派生させるには Pick と Omit を使用し、型を DRY に保ちます。

typescript
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };

// Record: typed key-value map
const scores: Record<string, number> = {
  math: 90,
  science: 85,
};

// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };

// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;

// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores);       // string[]
const values = Object.values(scores);   // number[]

Map と Set

Map と Set は完全な TypeScript サポートを持つ ES6 コレクションです。Map のキーは任意の型にできます(オブジェクトとは異なり、キーは文字列に強制変換されません)。Set は一意の値を格納します。WeakMap/WeakSet はキーのガベージコレクションを許可します — DOM 要素やオブジェクトにメタデータを付けるのに有用で、GC を妨げません。

typescript
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined

// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true

// Iteration (typed)
for (const [key, value] of map) {
  console.log(key, value.name);
}

for (const num of unique) {
  console.log(num);
}

// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();

タプルとラベル付きタプル

タプルは型付き位置を持つ固定長配列です。ラベル付きタプル(TS 4.0+)は可読性のための名前を追加します — 戻り値や CSV のようなデータに有用です。タプルによりインターフェースを作成せずに複数の戻り値が可能です。変更を防ぐには 'readonly' を使用します。タプルは配列とは異なります:[string, number] は (string | number)[] ではありません。

typescript
// Basic tuple
let point: [number, number] = [10, 20];

// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];

// Destructuring with labels
const [id, name, active] = user;

// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
  return [Math.floor(a / b), a % b];
}

const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2

// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // Error

列挙型と const 列挙型

列挙型は名前付き定数を作成します。文字列列挙が推奨されます(出力で読みやすく、逆マッピング問題なし)。const 列挙型はコンパイル時に消去されます(ランタイムコストゼロ)。単純なケースにはユニオン型('a' | 'b')の方が良いことが多いです — ランタイムコードなし、より良いツリーシェイキング。グループ化されたドキュメント付き定数には列挙型を使用してください。

typescript
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }

// String enum (recommended)
enum HttpStatus {
  OK = "200 OK",
  NotFound = "404 Not Found",
  ServerError = "500 Internal Server Error",
}

// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;

// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";

// Exhaustive switch
function handle(s: Status) {
  switch (s) {
    case "pending": return "loading";
    case "success": return "done";
    case "error": return "failed";
  }
}

不変データ

TypeScript は複数の不変性ツールを提供します:プロパティには 'readonly'、配列には ReadonlyArray、Readonly<T> ユーティリティ、リテラル型で深い読み取り専用には 'as const'。不変データは偶発的な変更を防ぎ、変更検出(React、Redux)を可能にします。不変更新にはスプレッド(...)を使用します — 変更されたフィールドで新しいオブジェクトを作成します。

typescript
// Readonly modifier
interface User {
  readonly id: number;
  name: string;
}

// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error

// Readonly utility type
type FrozenUser = Readonly<User>;

// as const: deep readonly with literal types
const config = {
  endpoint: "/api",
  methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]

// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new object
10

モジュールと名前空間

ES モジュール:インポートとエクスポート

TypeScript は ES モジュール構文(import/export)を使用します。名前付きエクスポートは明示的で、デフォルトエクスポートは単一の「メイン」エクスポートです。名前空間インポートには 'import * as' を使用します。モジュール解決は Node.js 規約(node_modules、拡張子)に従います。tsconfig.json で 'module' と 'moduleResolution' を設定します。

typescript
// math.ts - exporting
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export default function multiply(a: number, b: number): number {
  return a * b;
}

// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import

console.log(add(1, 2));      // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI);        // 3.14159

型専用インポート

'import type' は型のみをインポートします(コンパイル時に消去、ランタイムコードなし)。これにより循環依存と不要なランタイムインポートを回避します。TS 4.5+ では混合インポートでインライン 'type' 修飾子が可能です。バンドルサイズを削減するため、インターフェース、型エイリアス、列挙型(const の場合)には型専用インポートを使用してください。

typescript
// Type-only import (erased at runtime)
import type { User, Config } from "./types";

// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";

// Re-export types
export type { User } from "./types";

// Import type for interfaces and types
interface UserService {
  get(id: string): User;  // User from type-only import
}

// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoid

動的インポート

動的インポート(import())はモジュールをオンデマンドで読み込み、Promise を返します。これによりコード分割と遅延読み込みが可能になります — Web アプリのパフォーマンスに重要です。TypeScript はモジュール型を自動的に推論します。オプション機能、大規模ライブラリ、ルートベースのコード分割(React.lazy、Next.js dynamic)に使用します。

typescript
// Dynamic import returns a Promise
async function loadModule() {
  const module = await import("./heavy-module");
  module.doWork();
}

// Conditional loading
if (featureEnabled) {
  const { Feature } = await import("./feature");
  new Feature().init();
}

// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");

// With error handling
try {
  const lib = await import("./optional-lib");
  lib.run();
} catch (e) {
  console.log("Library not available");
}

宣言ファイルとモジュール拡張

宣言ファイル(.d.ts)は JS モジュール、CSS/PNG インポート、グローバル変数の型を記述します。モジュール拡張は既存のモジュール型を拡張します — Express Request、Express Response、またはサードパーティ型にプロパティを追加するのに有用です。これが passport のようなミドルウェアが req.user の型付けを追加する方法です。

typescript
// global.d.ts - declare ambient modules
declare module "*.css" {
  const content: string;
  export default content;
}

declare module "*.png" {
  const src: string;
  export default src;
}

// Module augmentation - extend existing types
import express from "express";

declare module "express" {
  interface Request {
    user?: { id: string; name: string };
  }
}

// Now req.user is typed
app.get("/", (req, res) => {
  console.log(req.user?.name);
});

名前空間(レガシー)

名前空間は TypeScript の ES6 以前のモジュールシステムです。関連コードを名前付きオブジェクトの下にグループ化します。新規プロジェクトには ES モジュール(import/export)を優先してください — 標準化され、ツリーシェイク可能で、バンドラで動作します。名前空間は .d.ts 宣言ファイルのグローバル型宣言とレガシーコードで残存します。

typescript
// Namespace: pre-ES6 way to organize code
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true

// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarations

tsconfig モジュール設定

tsconfig モジュール設定は TypeScript がインポートを処理する方法を制御します。'moduleResolution: node' は Node.js 解決(node_modules ルックアップ)を使用します。'esModuleInterop' は CommonJS からのデフォルトインポートを有効にします。'paths' はインポートエイリアス(@/components)を作成し、クリーンなインポートを実現します。'resolveJsonModule' は推論された型で .json ファイルのインポートを許可します。

typescript
{
  "compilerOptions": {
    "module": "ESNext",           // ES module output
    "moduleResolution": "node",   // Node-style resolution
    "esModuleInterop": true,      // allow default imports from CJS
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,    // import .json files
    "isolatedModules": true,      // each file is independent
    "baseUrl": "./src",           // base for non-relative imports
    "paths": {
      "@/*": ["./*"],             // path alias
      "@components/*": ["./components/*"]
    }
  }
}

// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"
11

非同期と Promise

Promise 型

Promise<T> はコアの非同期型です — T は解決された値の型です。TypeScript は .then() チェーンを通じて型を推論します。コールバックベースの API をラップするには 'new Promise()' を使用します。常に resolve/reject 値を型付けしてください。可読性とエラーハンドリングのために生の .then() チェーンより async/await を優先してください。

typescript
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id === 1) {
        resolve({ id: 1, name: "Alice" });
      } else {
        reject(new Error("User not found"));
      }
    }, 100);
  });
}

// Consuming a Promise
fetchUser(1)
  .then(user => console.log(user.name))
  .catch(err => console.error(err))
  .finally(() => console.log("done"));

// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
  // user is User, not any
  console.log(user.name); // string
});

async/await

async/await は Promise の糖衣構文です — 'await' は Promise が解決するまで一時停止します。async 関数は常に Promise を返します。並列実行には Promise.all() を使用します(逐次 await よりはるかに高速)。トップレベル await は ES2022+ の ES モジュールで動作します。TypeScript は await された値が Promise であるかチェックします。

typescript
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json() as Promise<User>;
}

// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);

// Sequential vs parallel
async function loadAll() {
  // Sequential (slow)
  const a = await getUser(1);
  const b = await getUser(2);

  // Parallel (fast)
  const [x, y] = await Promise.all([getUser(1), getUser(2)]);
  return { a, b, x, y };
}

非同期のエラーハンドリング

エラーハンドリングには async/await で try/catch を使用してください — .catch() よりクリーンです。TypeScript は型付き throw をサポートしません(catch 内の全エラーは unknown)なので、instanceof でナローイングします。予測可能なエラーには、例外の代わりに Result 型パターン(ok/error ユニオン)を検討してください — 型シグネチャでエラーハンドリングを明示的にします。

typescript
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
  try {
    const data = await fetch("/api/data");
    if (!data.ok) throw new Error(`HTTP ${data.status}`);
    return await data.text();
  } catch (error) {
    if (error instanceof Error) {
      console.error(error.message);
    }
    return "fallback";
  } finally {
    console.log("cleanup");
  }
}

// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
  }
}

// Result type as alternative to exceptions
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

async function safeFetch(url: string): Promise<Result<string>> {
  try {
    const res = await fetch(url);
    return { ok: true, value: await res.text() };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

Promise コンビネータ

Promise コンビネータは複数の非同期操作を調整します:all()(並列、フェイルファスト)、allSettled()(並列、全て待機)、race()(最初に確定)、any()(最初に成功)。依存データの読み込みには all() を、部分結果が必要な場合は allSettled() を、タイムアウトには race() を、冗長フェッチには any() を使用します。

typescript
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
]);

// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
  fetch("/api/a"),
  fetch("/api/b"),
]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.log(r.reason);
});

// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow"),
]);

// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
  fetch("/api/primary"),
  fetch("/api/fallback"),
]);

イベントループとマイクロタスク

JavaScript のイベントループはマイクロタスク(Promise コールバック、queueMicrotask)をマクロタスク(setTimeout、setInterval)より先に処理します。これが Promise がタイムアウトより先に解決する理由です。非同期反復(for await...of)は非同期イテラブルを消費します — ストリームに有用です。非同期ジェネレータ(async function*)は非同期イテラブルを生成し、遅延非同期シーケンスを可能にします。

typescript
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");

setTimeout(() => console.log("4: macrotask"), 0);

Promise.resolve().then(() => console.log("3: microtask"));

console.log("2: sync");
// Output: 1, 2, 3, 4

// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));

// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
  for await (const chunk of stream) {
    console.log(chunk.length);
  }
}

// Async generators
async function* generate() {
  for (let i = 0; i < 3; i++) {
    await new Promise(r => setTimeout(r, 100));
    yield i;
  }
}

並行性パターン

これらのパターンは TypeScript で完全に型安全です。デバウンスは呼び出しが N ms 停止するまで実行を遅延させます(検索入力)。スロットルは N ms あたり1回の呼び出しに制限します(スクロールハンドラ)。セマフォ/mapLimit は並行性を制御します — レート制限 API に有用です。Parameters<T> と ReturnType<T> はラッパーで関数シグネチャを保持します。

typescript
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

// Throttle
function throttle<T extends (...args: any[]) => void>(
  fn: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Semaphore (limit concurrency)
async function mapLimit<T, U>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<U>
): Promise<U[]> {
  const results: U[] = [];
  const executing: Promise<void>[] = [];
  for (const item of items) {
    const p = fn(item).then(r => results.push(r));
    executing.push(p);
    if (executing.length >= limit) {
      await Promise.race(executing);
      executing.splice(executing.findIndex(e => e === p), 1);
    }
  }
  await Promise.all(executing);
  return results;
}
12

エラーハンドリングとテスト

Unknown を伴う Try/Catch

TypeScript 4.4(useUnknownInCatchVariables)以降、キャッチされたエラーは 'unknown' です — 使用前にナローイングする必要があります。これにより存在しないプロパティへのアクセスを防ぎます。instanceof で特定のエラー型をチェックするか、フォールバックとして String() を使用します。一貫したエラー抽出のために getErrorMessage() ヘルパーを作成してください。

typescript
// In strict mode, catch is 'unknown' (not 'any')
try {
  JSON.parse("invalid");
} catch (error: unknown) {
  // Must narrow before using
  if (error instanceof SyntaxError) {
    console.error("JSON error:", error.message);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

// Helper function
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

カスタムエラークラス

カスタムエラークラスはエラーに構造化データ(code、statusCode、field)を追加します。常に super(message) を呼び出し、プロトタイプ(Object.setPrototypeOf)を設定して TypeScript/ES5 プロトタイプチェーンの問題を修正します。catch ブロックでエラー型を区別するには instanceof を使用します。このパターンは Express ミドルウェアと API エラーハンドラに不可欠です。

typescript
class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = "AppError";
    // Fix prototype chain (TS quirk)
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public field: string) {
    super(message, "VALIDATION_ERROR", 400);
    this.name = "ValidationError";
  }
}

// Usage
function createUser(input: unknown) {
  if (typeof input !== "object" || input === null) {
    throw new ValidationError("Invalid input", "body");
  }
}

try {
  createUser("bad");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.field, e.statusCode); // "body" 400
  }
}

Result 型パターン

Result 型(Rust から)は型シグネチャでエラーを明示的にします — 呼び出し元は成功と失敗の両方を処理する必要があります。例外とは異なり、コンパイラがエラーハンドリングを強制します。例外では過剰な予期される失敗(バリデーション、not-found)に使用します。真に予期しないエラー(バグ、システム障害)には例外を残してください。

typescript
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: "Division by zero" };
  }
  return { ok: true, value: a / b };
}

// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
  console.log(result.value); // number
} else {
  console.error(result.error); // string
}

// Utility helpers
function ok<T>(value: T): Result<T, never> {
  return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { ok: false, error };
}

アサーション関数

アサーション関数(asserts X)は条件が失敗した場合にスローし、その後型をナローイングします。'asserts value is string' は呼び出し後、value が string であることを TypeScript に伝えます。これは反復する if チェックよりクリーンです。境界(API 入力、設定)でのランタイムバリデーションに使用します。Zod や io-ts と組み合わせてスキーマバリデーションを行います。

typescript
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string

// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) throw new Error("undefined");
}

型安全な JSON 解析

JSON.parse は 'any' を返します — 安全ではありません。型ガードでラップしてランタイムで形状をバリデーションし、型をナローイングします。複雑なスキーマには Zod、io-ts、yup を使用します — 単一のスキーマ定義からランタイムバリデータと TypeScript 型の両方を生成します。これは API レスポンスとユーザー入力に不可欠です。

typescript
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) return parsed;
    return null;
  } catch {
    return null;
  }
}

// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && typeof (x as User).id === "number"
    && typeof (x as User).name === "string";
}

const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
  console.log(data.name); // User
}

// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));

網羅性チェック

網羅性チェックはユニオンの全ケースを処理することを保証します。デフォルトケースを 'never' に代入します — ユニオンに新しいバリアントを追加すると、新しい型が 'never' に代入できないため TypeScript がエラーになります。これによりコンパイル時に欠落ケースをキャッチします。判別ユニオン、Redux リデューサー、ステートマシンに不可欠です。

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.w * shape.h;
    default:
      // If a new shape is added, this errors
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'
13

デコレータとメタデータ

クラスデコレータ

クラスデコレータはコンストラクタ関数を受け取り、変更されたクラスを返せます。デコレータファクトリ(関数を返す)は引数を受け入れます。デコレータは実験的機能です — tsconfig で 'experimentalDecorators' を有効にしてください。NestJS、TypeORM、Angular で依存性注入とメタデータのために多用されます。

typescript
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
  return class extends target {
    constructor(...args: any[]) {
      super(...args);
      console.log(`Created ${target.name}`);
    }
  };
}

@Logged
class Service {
  constructor(public name: string) {}
}

const s = new Service("Auth");
// Logs: "Created Service"

// Decorator factory (with arguments)
function Prefix(prefix: string) {
  return function <T extends new (...args: any[]) => any>(target: T): T {
    return class extends target {
      message = prefix + " " + (this as any).name;
    };
  };
}

メソッドとプロパティデコレータ

メソッドデコレータは (target, propertyKey, descriptor) を受け取り、元のメソッドをラップできます — ロギング、キャッシュ、アクセス制御に有用です。プロパティデコレータは (target, key) を受け取り、メタデータの登録によく使用されます。descriptor.value が元の関数です。NestJS(@Get、@Post)と TypeORM(@Column)で一般的です。

typescript
// Method decorator: (target, key, descriptor)
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}(${args})`);
    return original.apply(this, args);
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }
}

new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"

// Property decorator: (target, key)
function Required(target: any, key: string) {
  // Store metadata for validation
  console.log(`Required: ${key}`);
}

パラメータデコレータとメタデータ

パラメータデコレータは (target, key, index) を受け取り、依存性注入(NestJS、Angular)に使用されます。'reflect-metadata' ポリフィルはランタイム型メタデータを可能にし — デコレータは Reflect.getMetadata('design:paramtypes') でパラメータ型にアクセスできます。これが DI コンテナが注入対象を知る方法です。tsconfig で 'emitDecoratorMetadata: true' で有効化します。

typescript
import "reflect-metadata";

// Parameter decorator
function Inject(target: any, key: string, index: number) {
  console.log(`Inject param ${index} of ${key}`);
}

class Service {
  constructor(@Inject private dep: any) {}
}

// Store and retrieve metadata
const METADATA_KEY = "design:type";

class Example {
  greet(name: string): void {}
}

// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)

const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]

アクセサデコレータ

アクセサデコレータはゲッター/セッターに適用されます。descriptor には get/set プロパティがあり、ラップできます。バリデーション、ロギング、列挙性の変更に使用します。バリデーションパターン(MaxLength、Min、Max)はセッターをラップしてランタイムで制約を強制します。これが class-validator(NestJS)が DTO バリデーションで動作する方法です。

typescript
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
  return function (
    target: any,
    key: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
  };
}

class Person {
  private _name: string = "";

  @Enumerable(true)
  get name(): string {
    return this._name;
  }

  set name(value: string) {
    this._name = value;
  }
}

const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)

// Validation decorator
function MaxLength(len: number) {
  return function (target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length > len) throw new Error("Too long");
        value = v;
      },
    });
  };
}

モダンデコレータ(TC39 Stage 3)

TypeScript 5.0 は TC39 Stage 3 デコレータ提案をサポートします — experimentalDecorators を置き換える標準化された API です。新しい API は (target, key, descriptor) の代わりにコンテキストオブジェクト(ClassMethodDecoratorContext)を使用します。よりクリーンで型安全で、最終的に JS 標準になります。新規プロジェクトにはこれを使用し、NestJS/Angular 互換性のためには実験的デコレータを残してください。

typescript
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object

function log<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  return function (this: This, ...args: Args): Return {
    console.log(`Calling ${String(context.name)}`);
    return target.call(this, ...args);
  };
}

class Service {
  @log
  greet(name: string): string {
    return `Hello, ${name}`;
  }
}

// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after construction

実践デコレータ:メモ化

このメモ化デコレータは引数に基づいてメソッド結果をキャッシュします — fibonacci のような高コストな純粋関数の劇的な高速化。キャッシュはインスタンスごとです(共有キャッシュには WeakMap を使用)。デコレータは横断的関心事に輝きます:ロギング、キャッシュ、バリデーション、アクセス制御、リトライロジック。インフラの関心事を分離することでビジネスロジックをクリーンに保ちます。

typescript
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const cache = new Map<string, Return>();
  return function (this: This, ...args: Args): Return {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = target.call(this, ...args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n < 2) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");

console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");
14

ユーティリティ型

Partial、Required と Readonly

Partial<T> は全プロパティをオプショナルにします — 一部のフィールドのみ変更する更新/パッチ操作に完適です。Required<T> は逆です。Readonly<T> は全プロパティをコンパイル時に不変にします。これらは最も一般的に使用されるユーティリティ型で、並行してオプショナル/読み取り専用インターフェースを手動で維持する必要性を排除します。

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }

function updateUser(id: number, changes: UserUpdate) {
  // Only provided fields are updated
  Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name

// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required

// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly

// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutation

Pick、Omit と Record

Pick<T, K> はプロパティのサブセットを抽出し、Omit<T, K> はプロパティを削除します — どちらも重複なしに派生型を作成します。Record<K, V> は特定のキーを持つマップ/辞書型を作成します。これらは DTO(データ転送オブジェクト)に不可欠です:id や createdAt のような自動生成フィールドを省いて User から CreateUser 型を派生させます。これにより型を DRY かつ同期に保ちます。

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  createdAt: Date;
}

// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }

// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;

ReturnType、Parameters と Awaited

ReturnType と Parameters は既存の関数から型を抽出します — シグネチャを複製したくない関数をラップまたは呼び出す際に invaluable です。Awaited<T> はネストされた Promise をアンラップし(Promise<Promise<T>> が T になる)、非同期関数の戻り値型に不可欠です。InstanceType はクラスコンストラクタからインスタンス型を取得します。これらにより型安全な関数合成が可能になります。

typescript
function fetchUser(id: number): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>

// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }

// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]

// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number

// ConstructorParameters<T>: parameters of a class constructor
class Point {
  constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]

// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // Point

Exclude、Extract と NonNullable

Exclude<T, U> はユニオンから型を削除し、Extract<T, U> は一致する型のみを保持します — どちらもユニオンメンバーに作用します。NonNullable<T> は null と undefined を削除します。これらは構成要素です:Omit は Pick<T, Exclude<keyof T, K>> として定義されます。結果ユニオンでエラー型を成功型から分離するなど、ユニオン型を動的にフィルタするのに Exclude/Extract を使用します。

typescript
type Role = "admin" | "user" | "guest" | "superadmin";

// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"

// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"

// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User

// Practical: filter union types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"

// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

カスタムユーティリティ型

カスタムユーティリティ型は特定のニーズのために組み込み型を合成します。Optional<T, K> は特定のフィールドのみをオプショナルにします(Partial より対象を絞る)。DeepPartial/DeepReadonly はネストされたオブジェクトに再帰的に適用します — 設定とステートツリーに有用です。Mutable の -readonly 修飾子は readonly を削除します。これらのパターンはマップ型と条件型が強力な型レベルプログラミングのために組み合わさる方法を示します。

typescript
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface User {
  id: number;
  name: string;
  email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }

// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Mutable (remove readonly from all properties)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
  [K in keyof T]: T[K] extends never ? never : K;
}[keyof T];
15

条件型

基本条件型(T extends U ? X : Y)

条件型(T extends U ? X : Y)は型レベルの条件に基づいて型を選択します — 型の三項演算子のようなものです。TypeScript の型レベルプログラミングの基盤です。T がユニオンの場合、条件は各メンバーに分配されます(分配条件型)。infer キーワードはパターン内から型を抽出し、配列から要素型や Promise から解決型を引き出します。

typescript
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y

// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;    // string
type B = ElementOf<number[]>;    // number
type C = ElementOf<string>;      // never (not an array)

// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type D = UnwrapPromise<Promise<number>>;  // number
type E = UnwrapPromise<string>;           // string (not a promise)

// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type F = IsFunction<() => void>;  // true
type G = IsFunction<number>;      // false

// Conditional types are evaluated lazily and distribute over unions

infer キーワード(型抽出)

infer キーワードは条件型の extends 句内で型変数を宣言し、その位置にマッチする型をキャプチャします。ReturnType、Parameters、Awaited の実装方法です。infer は再帰的に使用でき(Unwrap<Promise<Promise<T>>>)、ネストされた型を完全にアンラップします。複雑なジェネリック構造から型を抽出する主要なツールです。

typescript
// infer extracts a type from within a pattern

// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string

// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)

// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number

// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;

// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;

分配条件型

条件型はユニオンに分配されます:ToArray<A | B> を適用すると ToArray<A> | ToArray<B> になり、(A | B)[] にはなりません。これが Exclude と NonNullable がユニオンメンバーをフィルタする方法です — 除外された型には 'never' を返し、ユニオン内で崩壊します。分配を防ぐには両側を括弧で囲みます:[T] extends [U]。フィルタリングには通常分配が望ましいですが、「ユニオン全体をラップ」操作には非分配が必要です。

typescript
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y  applied to  A | B  becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)

type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]

// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[]  — a single array of the union

// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number  (null and undefined filtered out)

// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;

条件型の制約

条件型はネストして型レベルの判別(型の switch 文のような)を作成できます。infer と組み合わせて、ジェネリックパラメータから型を抽出・派生させます。これが React がコンポーネント定義から prop 型を派生させ、ルーティングライブラリがパス文字列からパラメータ型を抽出する方法です。制約(T extends any[])は条件が要素型を抽出する前に 入力が有効であることを保証します。

typescript
// Use conditional types to constrain and derive types

// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type

// Conditional with multiple branches (nested)
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;     // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>;         // "object"

// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
  return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // number

テンプレートリテラル型(文字列操作)

テンプレートリテラル型は型レベルの文字列操作を可能にします — 連結、ケース変換、パターンマッチング。条件型と infer と組み合わせて、パス文字列を解析してルートパラメータを抽出、イベントハンドラ名を生成、型安全なプロパティアクセサを構築できます。これが Next.js や tRPC のようなフレームワークが文字列リテラルからエンドツーエンドの型安全な API を作成する方法です。

typescript
// Template literal types: type-level string operations

type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error

// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">;    // "Foo"

// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Extract route parameters
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}/${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}/${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"

// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T ? Get<T[Key], Rest> : never
    : P extends keyof T ? T[P] : never;
16

マップ型

基本マップ型

マップ型はオブジェクトのキーを反復して各プロパティを変換します — [K in keyof T] が構文です。Partial、Readonly、Pick、その他のユーティリティ型の実装方法です。プロパティ型の変更(T[K] | null)、修飾子の追加(? や readonly)、または値の型の完全な置き換えができます。マップ型は TypeScript の型変換システムの骨格です。

typescript
// Mapped types transform each property of an existing type

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }

// Change all property types to a specific type
type Stringify<T> = {
  [K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }

'as' によるキーリマッピング

キーリマッピング(as 句、TS 4.1+)でマッピング中にキーの名前変更やフィルタができます。テンプレートリテラル型でキー名を変換します(接頭辞の追加、getter への変換、大文字化)。キーに 'never' を返すと削除されます — これがプロパティをフィルタする方法です。条件型と組み合わせて、データスキーマをバリデーションスキーマに、API 型をフォーム型に変換する強力な変換が可能です。

typescript
// Remap keys using 'as' (TypeScript 4.1+)

// Add a prefix to all keys
type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

interface User {
  id: number;
  name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }

// Convert keys to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Filter out keys by returning never
type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }

// Make keys UPPERCASE
type UpperKeys<T> = {
  [K in keyof T as Uppercase<string & K>]: T[K];
};

修飾子:+、-、?、readonly

+ と - 修飾子はプロパティ修飾子を追加または削除します。-? はオプショナル性を削除し(オプショナルフィールドを必須に)、-readonly は不変性を削除します。これが Required<T> と Mutable パターンの動作方法です。+ 接頭辞はオプショナルです(readonly は +readonly と同じ)が、削除には - が必要です。型変換中のプロパティ特性のきめ細かい制御が可能です。

typescript
// Add (+) or remove (-) modifiers

// Remove optional (?) modifier: -?
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

interface OptionalUser {
  id?: number;
  name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now

// Remove readonly modifier: -readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface FrozenConfig {
  readonly port: number;
  readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now

// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
  +readonly [K in keyof T]: T[K];
};

// Add optional modifier: +?
type MakeOptional<T> = {
  [K in keyof T]+?: T[K];
};

同型マップ型

同型マップ型([K in keyof T])はソース型からプロパティ修飾子(readonly、?)を保持します — これが Pick<User, 'id'> が id を readonly のまま保持する理由です。非同型マッピング(例:[K in string])は修飾子を保持しません。これは型派生の際に重要です:readonly フィールドを持つ型の同型 Partial はそれらのフィールドを readonly のまま保持します(ただしオプショナルになります)。同型性の理解は修飾子が変換を生き残るかの予測に役立ちます。

typescript
// Homomorphic mapped types preserve modifiers from the original type

interface User {
  readonly id: number;
  name: string;
  email?: string;
}

// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
  [K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User

// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
  [K in string]: T; // not keyof T — loses modifiers
};

// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!

// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
  [K in keyof T]?: T[K]; // Partial is homomorphic
};

スキーマからバリデーション型の構築

これがフォームライブラリ(React Hook Form、Formik)とバリデーションライブラリ(Zod、Yup)が型安全性を維持する方法です — マップ型を使用してデータインターフェースからバリデータとフォーム型を派生させます。User にフィールドを追加すると、バリデータとフォーム型も自動的にそれを要求し、ドリフトを防ぎます。これはマップ型の実世界の力を示します:単一の真実のソース(インターフェース)が複数の派生型を駆動します。

typescript
// Practical: derive a validator type from a data type

// Given a data interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Create a validator type where each field is a validation function
type Validator<T> = {
  [K in keyof T]: (value: T[K]) => boolean;
};

const userValidator: Validator<User> = {
  id: (v) => v > 0,
  name: (v) => v.length > 0,
  email: (v) => v.includes("@"),
};

// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
  value: T;
  error: string | null;
  touched: boolean;
};

type Form<T> = {
  [K in keyof T]: FormField<T[K]>;
};

const userForm: Form<User> = {
  id: { value: 1, error: null, touched: false },
  name: { value: "Alice", error: null, touched: true },
  email: { value: "", error: "Required", touched: true },
};

// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.
17

型ガードとナローイング

typeof と instanceof ナローイング

TypeScript はランタイムチェックに基づいて型をナローイングします。typeof はプリミティブをナローイングし(string、number、boolean など)、instanceof はクラスインスタンスをナローイングします。真偽値チェック(if (value))は null/undefined/0/''/false を除外します。ナローイングは条件が成り立つブランチ内に適用されます。これが TypeScript でランタイムチェックに型情報を持たせる方法で、明示的キャストの必要性を排除します。

typescript
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + value;
    // padding is narrowed to 'number' here
  }
  return padding + value;
  // padding is narrowed to 'string' here
}

// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow(); // OK — narrowed to Cat
  } else {
    animal.bark(); // OK — narrowed to Dog
  }
}

// typeof returns: "string" | "number" | "boolean" | "symbol"
//   "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)

// Truthiness narrowing
function process(value?: string) {
  if (value) {
    console.log(value.toUpperCase()); // value is string (not undefined)
  }
}

in 演算子と判別ユニオン

'in' 演算子はプロパティの存在に基づいてナローイングします。判別ユニオンは共有リテラルプロパティ('kind' や 'type' など)をタグとして使用し — それで switch すると完全なプロパティアクセスで正しいバリアントにナローイングします。これは TypeScript における直和型 / 代数的データ型の等価物です。Redux アクション、ステートマシン、複数形状の API レスポンスの標準パターンです。判別子には常にリテラル型を使用してください。

typescript
// 'in' operator checks for a property — narrows to types that have it

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // narrowed to Fish
  } else {
    animal.fly(); // narrowed to Bird
  }
}

// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // narrowed by kind
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)

カスタム型ガード関数

カスタム型ガード(戻り値型 'x is Type')で複雑なランタイムチェックを再利用可能な型をナローイングする関数にカプセル化できます。アサーション関数(asserts x is T)はブール値を返す代わりにスローします — 呼び出し後の全コードでナローイングします。型ガードを使用して信頼できないデータ(JSON.parse、API レスポンス)をバリデーションし、型システムに取り込みます。これがランタイムバリデーションとコンパイル時型の橋渡しです。

typescript
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true

interface User {
  id: number;
  name: string;
}

function isUser(obj: any): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    typeof obj.id === "number" &&
    typeof obj.name === "string"
  );
}

const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');

if (isUser(data)) {
  console.log(data.name); // data is narrowed to User
}

// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
  return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}

// Type guard for discriminated unions
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
  return r.success;
}

// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("Expected value to be defined");
  }
}

never による網羅性チェック

網羅性チェックは 'never' 型を使用して全ユニオンバリアントが処理されることを保証します。ユニオンに新しいバリアントを追加したがケースを忘れた場合、デフォルトブランチの 'never' 代入がコンパイルエラーになります。assertNever ヘルパーはランタイムでスローし、コンパイル時に欠落ケースでエラーになります。これは判別ユニオンの最も価値あるパターンです — ケースを忘れた時にコンパイラが教えてくれます。

typescript
// The 'never' type ensures all cases are handled

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; size: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    default:
      // If we add a new shape but forget a case, this line
      // becomes a compile error because 'shape' would not be 'never'
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'

// Helper function pattern
function assertNever(x: never): never {
  throw new Error("Unexpected: " + JSON.stringify(x));
}

function process(shape: Shape) {
  switch (shape.kind) {
    case "circle": return;
    case "square": return;
    default: return assertNever(shape); // compile error if a case is missing
  }
}

配列メソッドによるナローイング

Array.filter はコールバックがブール値を返すため、デフォルトでは要素型をナローイングしません。ナローイングするにはカスタム型ガード関数(pet is Dog)を渡します — すると filter はナローイングされた配列型を返します。TypeScript は if チェックに基づいてコールバック本体内(forEach、map)でもナローイングします。Array.isArray は unknown/any を配列型にナローイングする組み込み型ガードです。

typescript
// TypeScript narrows through filter, map, and control flow

interface Pet {
  name: string;
  speak(): void;
}

class Dog implements Pet {
  name = "Rex";
  speak() { console.log("Woof"); }
  fetch() { console.log("Fetching!"); }
}

class Cat implements Pet {
  name = "Whiskers";
  speak() { console.log("Meow"); }
}

const pets: Pet[] = [new Dog(), new Cat(), new Dog()];

// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
  return pet instanceof Dog;
}

const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog

// Narrowing in forEach/callbacks
pets.forEach((pet) => {
  if (isDog(pet)) {
    pet.fetch(); // narrowed to Dog inside callback
  }
});

// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
  if (Array.isArray(input)) {
    input.length; // OK — input is any[] (or unknown[])
  }
}
18

型推論

変数と戻り値型の推論

TypeScript は初期化子と return 文から型を推論するため、明示的な注釈はほとんど不要です。変数は一般型に拡大されます(let x = 10 は 10 ではなく number を推論)。'as const' は拡大を防ぎます:リテラルをリテラルのままにし、オブジェクトを読み取り専用にし、配列を読み取り専用タプルにします。typeof colors[number] はタプル要素型のユニオンを抽出します — 配列から列挙型のような型を派生させる一般的なパターンです。

typescript
// TypeScript infers types when you don't annotate

// Variable inference
let count = 0;          // number
let name = "Alice";     // string
let items = [1, 2, 3];  // number[]
let mixed = [1, "two"]; // (string | number)[]

// Function return type inference
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// const assertions for literal types
const x = 10;        // number (widened)
const y = "hello";   // string (widened)
const z = { a: 1 };  // { a: number }

const x2 = 10 as const;       // 10 (literal type)
const y2 = "hello" as const;  // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }

// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"

コンテキスト型付け

コンテキスト型付けは期待される型を後方に式に流します。型付き変数に関数を代入すると、パラメータ型はターゲット型から推論されます。これがイベントハンドラ、配列コールバック、オブジェクトリテラルが型注釈を必要としないことが多い理由です。経験則:関数シグネチャ(公開 API のパラメータと戻り値型)は注釈し、ローカルとコールバックは推論に任せます。

typescript
// Contextual typing: the expected type influences the inferred type

// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
  // mouseEvent is inferred as MouseEvent (from onmousedown's type)
  console.log(mouseEvent.button);
};

// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]

// Contextual typing for object literals
interface User {
  name: string;
  age: number;
}
const user: User = {
  name: "Alice", // inferred as string (from User.name)
  age: 30,
};

// Contextual typing with discriminated unions
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "scroll"; scrollTop: number };

function handle(e: Event) {
  if (e.type === "click") {
    console.log(e.x, e.y); // narrowed
  }
}

// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation needed

最良共通型(ユニオン推論)

複数の値から推論する場合(配列リテラルなど)、TypeScript は「最良共通型」を見つけます — 通常はスーパータイプまたはユニオンです。[Dog, Cat] の配列は (Dog | Cat)[] ではなく Animal[](共通の基底)として推論されます。ユニオンを得るには明示的に注釈します。条件付き戻り値は全ブランチのユニオンを推論します。これの理解は明示的注釈が必要な時と推論で十分な時の予測に役立ちます。

typescript
// When inferring from multiple values, TS finds the best common type

// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]

// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]

// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }

const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])

// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];

// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]

// Return type inference with conditionals
function getValue(flag: boolean) {
  return flag ? 42 : "hello"; // inferred as number | string
}

制御フロー分析

TypeScript は制御フロー分析を実行します — if/else、return、代入、論理演算子を通じて型がナローイングおよび拡大されるかを追跡します。型はチェック後にナローイングされ、変数が再代入されるまで維持されます。早期リターン(ガード節)は特に効果的です:'if (value === null) return' の後、関数の残りは value が null でないことを知ります。これがガード節スタイルのコードが TypeScript でうまく機能する理由です。

typescript
// TypeScript tracks types through control flow

function example(value: string | number | null) {
  // After this check, 'value' is string | number in this branch
  if (value === null) {
    return;
  }

  // value is now string | number (null narrowed out)
  console.log(value);

  if (typeof value === "string") {
    return value.toUpperCase(); // value is string
  }

  // value is now number (string and null narrowed out)
  return value.toFixed(2);
}

// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number

// Narrowing with && and ||
function process(input?: string) {
  const value = input && input.trim(); // string | undefined
  const safe = input || "default"; // string (undefined narrowed out)
}

// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCase

satisfies 演算子(TS 4.9+)

'satisfies' 演算子(TS 4.9+)は値が型に適合することをバリデーションしながら、最も具体的な推論型を保持します — 型注釈が拡大するのとは異なります。これは設定、ルートマップ、テーマオブジェクトに理想です:構造が正しいことのコンパイル時バリデーションを得つつ、プロパティアクセスは精密なリテラル型を返します。リテラル保持と構造バリデーションの両方に 'as const' と組み合わせてください。

typescript
// 'satisfies' checks a value matches a type WITHOUT widening it

// Problem: annotation widens the type
const config1: Record<string, string | number> = {
  port: 8080,
  host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)

// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
  port: 8080,
  host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated

// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;

const routes = {
  "/users": { method: "GET", handler: () => fetchUsers() },
  "/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;

routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)

// 'as const' + 'satisfies': literal types + validation
const colors = {
  primary: "#ff0000",
  secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;
19

宣言ファイルとモジュール拡張

.d.ts 宣言ファイルの作成

.d.ts ファイルは型宣言(実装なし)を含みます — JavaScript コードの型を記述します。'declare module' で型のない npm パッケージに型を追加します。'declare global' で Window のようなグローバル型を拡張します。アンビエント宣言は TypeScript に「これは実行時に存在する、信じて」と伝えます。これがレガシー JS、ブラウザ API、ビルド時注入変数を型システムに統合する方法です。

typescript
// types/my-module.d.ts — describes the types of a JS library

// Declare a module (for libraries without types)
declare module "untyped-lib" {
  export function greet(name: string): string;
  export const version: string;
  export interface Config {
    timeout: number;
    retries: number;
  }
}

// Ambient declarations for global variables
declare global {
  interface Window {
    myCustomProp: string;
    myApp: { init: () => void };
  }
}

// Now window.myCustomProp is typed
// window.myCustomProp; // string

// Declaring a global function
declare function myGlobalFn(x: number): string;

// Declaring a global namespace
declare namespace MyLib {
  function doSomething(): void;
  const version: string;
}

// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constants

モジュール拡張(既存型の拡張)

モジュール拡張は他のモジュールの既存型を拡張します — 元のソースを変更せずにインターフェースにプロパティを追加します。これが Express ミドルウェア(passport など)が req.user を追加し、サードパーティライブラリ型を拡張する方法です。'declare module' 構文がモジュールの型空間を再オープンします。拡張はグローバルに有効にするためモジュール(import/export を持つファイル)内になければなりません。

typescript
// Module augmentation: add to an existing module's types

// Extend an interface from another module
import express from "express";

declare module "express" {
  interface Request {
    user?: {
      id: number;
      name: string;
    };
    // Now req.user is typed on all Express requests
  }
}

// Augment a third-party module
declare module "axios" {
  export interface AxiosRequestConfig {
    retryCount?: number; // add a custom config option
  }
}

// Augment a global interface
declare global {
  interface Array<T> {
    last(): T | undefined; // add a custom array method
  }
}

// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
  return this[this.length - 1];
};

[1, 2, 3].last(); // 3 — now typed!

// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.

トリプルスラッシュディレクティブ

トリプルスラッシュディレクティブ(///)は TypeScript に追加ファイルや型パッケージを含めるよう指示する特別なコンパイラコメントです。最も一般的なのは @types/node を含めるための /// <reference types='node' /> です。モダンな tsconfig.json の 'types' と 'lib' オプションにより、これらはほとんど不要です — 設定ベースの設定を優先してください。主に .d.ts ファイルとレガシーコードで見られます。宣言ファイルを読む際の理解に役立ちます。

typescript
// Triple-slash directives are special comments processed by TS

/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)

/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)

/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")

// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />

declare function readFile(path: string): Buffer; // Buffer from @types/node

// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.

// Prefer tsconfig.json settings:
// {
//   "compilerOptions": {
//     "types": ["node"],      // instead of /// <reference types="node" />
//     "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
//   }
// }

パッケージでの型の公開

npm パッケージで TypeScript 型を公開するには、package.json の 'types' を .d.ts ファイルに設定し、tsconfig で 'declaration: true' を有効にします。コンシューマーはパッケージをインストール時に自動的に型を取得します。declarationMap で「定義へ移動」がソース .ts ファイルにジャンプできます。バンドル型のないライブラリには、DefinitelyTyped プロジェクト(@types/package)がコミュニティメンテナンスの宣言を提供します。

typescript
// package.json for a library with TypeScript types
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",  // <-- points to the type declarations
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  }
}

// tsconfig.json for building a library
{
  "compilerOptions": {
    "declaration": true,        // generate .d.ts files
    "declarationMap": true,     // generate .d.ts.map (source maps for types)
    "sourceMap": true,          // generate .js.map
    "outDir": "./dist",
    "rootDir": "./src",
    "composite": true           // enable project references
  }
}

// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!

// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express

// Check if types exist: https://www.typescriptlang.org/dt/search

型専用インポートとエクスポート

'import type' は型情報のみをインポートします — 実行時に完全に消去され、バンドルサイズを削減し、循環依存の問題を回避します。インターフェース、型エイリアス、型専用再エクスポートに使用します。インライン 'import { x, type Y }' 構文(TS 4.5+)は値と型のインポートをクリーンに混在させます。verbatimModuleSyntax(TS 5.0+)はこれを厳格に強制します。型位置のみで使用するものをインポートする場合は常に 'import type' を優先してください。

typescript
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";

// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies

// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only

// 'export type' re-exports types only
export type { User, Config } from "./types";

// Interface vs Type for declarations
interface IUser { name: string; }  // can be augmented/merged
type TUser = { name: string };     // cannot be merged, more flexible

// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
//   "compilerOptions": { "verbatimModuleSyntax": true }
// }
20

tsconfig.json オプション

コアコンパイラオプション

tsconfig.json は TypeScript のコンパイル方法を制御します。'target' は出力 JS バージョンを設定し、'module' はモジュールシステムを設定します。'strict: true' が最も重要な設定です — 全ての厳格な型チェック(noImplicitAny、strictNullChecks など)を有効にします。'lib' は利用可能な組み込み API を決定します(ブラウザ用 DOM、モダン JS 機能用 ES2022)。新規プロジェクトは常に strict: true で開始してください。

typescript
{
  "compilerOptions": {
    "target": "ES2022",        // JS version to compile to
    "module": "ESNext",        // module system (ESNext, CommonJS, etc.)
    "moduleResolution": "bundler", // how modules are resolved
    "lib": ["ES2022", "DOM"],  // available type definitions
    "outDir": "./dist",        // output directory for compiled JS
    "rootDir": "./src",        // root of source files
    "sourceMap": true,         // generate .map files for debugging
    "declaration": true,       // generate .d.ts files (for libraries)
    "removeComments": true,    // strip comments from output

    // Type checking strictness
    "strict": true,            // enable ALL strict checks (recommended)
    "noImplicitAny": true,     // error on implicit 'any'
    "strictNullChecks": true,  // null/undefined are separate types
    "noUnusedLocals": true,    // error on unused local variables
    "noUnusedParameters": true,// error on unused function params
    "noImplicitReturns": true, // error if not all paths return
    "noFallthroughCasesInSwitch": true,

    "esModuleInterop": true,   // allow default imports from CommonJS
    "skipLibCheck": true,      // skip type checking of .d.ts files
    "forceConsistentCasingInFileNames": true
  }
}

厳格モードフラグの解説

厳格モードは厳格性フラグのバンドルです。strictNullChecks が最も影響力大 — null/undefined を別個の型にし、明示的な処理を強制します(ランタイムクラッシュの#1原因)。noImplicitAny は暗黙の型侵食を防ぎます。strictPropertyInitialization は未初期化のクラスフィールドをキャッチします(確定代入には ! を使用、またはコンストラクタで初期化)。新規プロジェクトでは常に厳格モードを有効にしてください — 初期コストは安全性の価値があります。

typescript
{
  "compilerOptions": {
    // 'strict: true' enables ALL of these:

    "strictNullChecks": true,
    // null and undefined are NOT assignable to other types
    // without explicit union. Forces null handling.
    let x: string = null; // ERROR (without this, it's allowed)

    "noImplicitAny": true,
    // Parameters/variables can't be implicitly 'any'
    function fn(x) { } // ERROR: x is implicitly any

    "strictFunctionTypes": true,
    // Function parameter types checked contravariantly
    // (catches unsafe function assignments)

    "strictBindCallApply": true,
    // bind/call/apply are type-checked

    "strictPropertyInitialization": true,
    // Class properties must be initialized or declared with !
    class User {
      name: string; // ERROR: not initialized
      name2!: string; // OK: definite assignment assertion
    }

    "noImplicitThis": true,
    // 'this' must have a known type (no implicit any)

    "alwaysStrict": true
    // Emit "use strict" in every file
  }
}

モジュール解決戦略

moduleResolution はインポートパスの解決方法を制御します。'node' は古典的戦略、'bundler'(TS 5.0+)は Vite のようなモダンバンドラに一致し package.json exports をサポートします。'nodenext' は厳格な ESM です(拡張子が必要)。paths でインポートエイリアス(@/ → src/)を作成でき、バンドラ設定(Vite の resolve.alias など)でミラーリングする必要があります。baseUrl + paths が深い相対インポート(../../../)を回避する標準的な方法です。

typescript
{
  "compilerOptions": {
    // How TS resolves import paths

    "moduleResolution": "node",    // classic Node.js resolution
    // Looks for: file.ts, file/index.ts, node_modules/file

    "moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
    // Matches how bundlers resolve: supports import maps,
    // conditional exports, no file extension requirement

    "moduleResolution": "nodenext", // Node.js ESM resolution (strict)
    // Requires file extensions in imports: import "./foo.js"

    // Path mapping (aliases)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],           // import "@/components/Button"
      "@utils/*": ["src/utils/*"],
      "@components": ["src/components/index.ts"]
    }

    // rootDirs: virtual directories that map to the same location
    "rootDirs": ["src", "generated"],
    // imports between src/ and generated/ resolve as if same dir
  }
}

// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.ts

プロジェクト参照(モノレポ)

プロジェクト参照は大規模コードベースを独立してコンパイルされるサブプロジェクトに分割します — モノレポに不可欠です。各プロジェクトは composite: true を持ち、宣言を生成します。References がプロジェクト間の依存関係を宣言します。tsc --build (-b) は依存順にコンパイルし、変更されたもののみ再ビルドします。これにより大規模コードベースの型チェックが劇的に高速化し、パッケージ間のアーキテクチャ境界を強制します。

typescript
// tsconfig.json (root — references sub-projects)
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,          // required for project references
    "declaration": true,        // must emit declarations
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { /* ... */ },
  "references": [
    { "path": "../shared" }     // depends on shared
  ]
}

// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically

// Build command: tsc --build (or tsc -b)

一般的な tsconfig レシピ

異なるプロジェクトタイプには異なる tsconfig 設定が必要です。React/Vite は jsx: 'react-jsx' と noEmit(Vite がコンパイル)を使用します。Node.js は CommonJS(または ESM 用 NodeNext)と types: ['node'] を使用します。ライブラリは .d.ts 出力に declaration: true とより広い互換性のためにより低い target が必要です。isolatedModules は Vite/esbuild に必要です(各ファイルが独立してコンパイル可能である必要)。テストファイルと node_modules は常にビルドから除外してください。

typescript
// React + Vite project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",          // React 17+ JSX transform
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,             // Vite handles compilation
    "isolatedModules": true,    // required by Vite/esbuild
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

// Node.js backend project
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",        // or NodeNext for ESM
    "moduleResolution": "node",
    "lib": ["ES2022"],
    "types": ["node"],           // @types/node
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src"]
}

// Library project (publishes to npm)
{
  "compilerOptions": {
    "target": "ES2020",          // broader compatibility
    "module": "ESNext",
    "declaration": true,         // emit .d.ts
    "declarationMap": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
21

デコレータ

クラスデコレータ

クラスデコレータはコンストラクタを受け取り、変更されたクラスを返せます。実験的です(experimentalDecorators: true が必要)。NestJS と TypeORM で一般的です。

typescript
function logged<T extends new (...args: any[]) => any>(con: T) {
    return class extends con {
        created = new Date().toISOString();
    };
}
@logged
class Service { constructor(public name: string) {} }

メソッドデコレータ

メソッドデコレータは (target, key, descriptor) を受け取ります。descriptor.value をラップしてロギング、キャッシュ、バリデーションを可能にします。これが NestJS インターセプタの動作方法です。

typescript
function log(target: any, key: string, desc: PropertyDescriptor) {
    const orig = desc.value;
    desc.value = function(...args: any[]) {
        console.log(`Calling ${key}`, args);
        return orig.apply(this, args);
    };
}
class Calc { @log add(a: number, b: number) { return a + b; } }

プロパティデコレータ

プロパティデコレータは (target, key) を受け取ります。Object.defineProperty でバリデーション用ゲッター/セッターを作成します。class-validator で DTO バリデーションに使用されます。

typescript
function required(target: any, key: string) {
    let val: any;
    Object.defineProperty(target, key, {
        get() { return val; },
        set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
    });
}

パラメータデコレータ

パラメータデコレータは (target, methodKey, parameterIndex) を受け取ります。バリデーション用のメタデータリフレクションと共に使用されます。class-validator と NestJS がこれを使用します。

typescript
function Min(min: number) {
    return (target: any, key: string, idx: number) => {
        console.log(`${key} param ${idx} >= ${min}`);
    };
}
class Order { create(@Min(0) qty: number) { return qty; } }

デコレータファクトリ

デコレータファクトリはデコレータ関数を返し、設定を可能にします。外側の関数がパラメータを受け取り、内側が実際のデコレータです。

typescript
function Retry(times: number) {
    return function(target: any, key: string, desc: PropertyDescriptor) {
        const orig = desc.value;
        desc.value = async function(...args: any[]) {
            for (let i = 0; i < times; i++)
                try { return await orig.apply(this, args); }
                catch (e) { if (i === times - 1) throw e; }
        };
    };
}
22

モジュール拡張

組み込み型の拡張

モジュール拡張は既存の型を拡張します。declare global で Array のような組み込み型を拡張できます。ランタイム実装も提供する必要があります。

typescript
declare global {
    interface Array<T> {
        last(): T | undefined;
        chunk(size: number): T[][];
    }
}
Array.prototype.last = function() { return this[this.length - 1]; };

ライブラリ型の拡張

モジュール拡張はサードパーティライブラリの型を拡張します。declare module でモジュール型を再オープンします。フレームワークオブジェクトにカスタムプロパティを追加するために不可欠です。

typescript
declare module 'express' {
    interface Request {
        user?: { id: string; role: string };
    }
}
app.get('/profile', (req, res) => {
    const userId = req.user?.id;  // Typed!
});

Window の拡張

Window を拡張すると型安全性付きでカスタムグローバルプロパティを追加できます。デバッグツールやアナリティクスにアプリ状態を公開するのに有用です。

typescript
declare global {
    interface Window {
        myApp: { init: () => void; version: string };
    }
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };

CSS Modules

CSS Modules には型宣言が必要です。宣言が .module.css インポートをクラス名のレコードにマッピングします。CSS クラス参照の自動補完を可能にします。

typescript
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />

Vue プラグイン

Vue や他のフレームワークはプラグイン型付けにモジュール拡張を使用します。ComponentCustomProperties がインスタンスプロパティを追加します。型安全なプラグインを可能にします。

typescript
declare module 'vue' {
    interface ComponentCustomProperties {
        $auth: { login: () => Promise<void> };
    }
}
export default defineComponent({
    methods: { async login() { await this.$auth.login(); } }
});
23

宣言マージ

インターフェースのマージ

同名のインターフェースは自動的にマージされます。全メンバーが単一のインターフェースの一部になります。インターフェースをファイル間で分割するのに有用です。

typescript
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };

名前空間のマージ

同名の名前空間はエクスポートをマージします。これにより名前空間の内容をファイル間で分割できます。新規コードには ES モジュールが推奨されます。

typescript
namespace Utils {
    export function log(msg: string) { console.log(msg); }
}
namespace Utils {
    export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');

関数との名前空間

名前空間は関数、クラス、列挙型とマージできます。名前空間が関数に静的プロパティを追加します。Moment.js や類似ライブラリで使用されます。

typescript
function Counter() { Counter.count++; }
namespace Counter {
    export let count = 0;
    export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count);  // 2

クラスとのマージ

名前空間とクラスのマージは静的メンバーとネストされた型を追加します。名前空間はネストされた型になるインターフェースをエクスポートできます。

typescript
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
    export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };

禁止されるマージ

クラスは他のクラスとマージできません。変数はマージできません。関数はオーバーロードとしてマージされます。列挙型は名前空間とマージできます。

typescript
// Cannot merge classes
class A { x = 1; }
class A { y = 2; }  // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }
24

型ナローイング

typeof と instanceof

typeof はプリミティブ型をナローイングします。instanceof はクラスタイプをナローイングします。TypeScript はこれらのチェックを理解し、各ブランチで型をナローイングします。

typescript
function process(value: string | number | Date) {
    if (typeof value === 'string') return value.toUpperCase();
    if (typeof value === 'number') return value.toFixed(2);
    if (value instanceof Date) return value.toISOString();
}

in 演算子

in 演算子はプロパティが存在するかチェックし、型をナローイングします。異なるプロパティ名を持つ判別ユニオンに有用です。

typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
    if ('meow' in animal) animal.meow();
    else animal.bark();
}

判別ユニオン

判別ユニオンは共通プロパティ(判別子)を使用して型をナローイングします。網羅的チェックのために判別子で switch します。バリアント型の最も安全なパターンです。

typescript
type Result =
    | { status: 'success'; data: string }
    | { status: 'error'; message: string };
function handle(r: Result) {
    switch (r.status) {
        case 'success': console.log(r.data); break;
        case 'error': console.log(r.message); break;
    }
}

型述語

型述語(x is T)でカスタムナローイング関数を可能にします。true を返すと T にナローイングし、false で除外された型にナローイングします。TypeScript は述語を盲目的に信頼します。

typescript
function isFish(pet: Fish | Bird): pet is Fish {
    return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
    if (isFish(pet)) pet.swim();
    else pet.fly();
}

アサーション関数

アサーション関数は条件が失敗した場合スローし、後続コードの型をナローイングします。asserts x is T で T にナローイングします。冗長な null チェックを排除します。

typescript
function assertDefined<T>(value: T | null | undefined): asserts value is T {
    if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
    assertDefined(user);
    console.log(user.name);  // User (not undefined)
}
25

テンプレートリテラル型

基本テンプレートリテラル

テンプレートリテラル型は文字列パターンを作成します。文字列をテンプレートに一致するよう制約します。API エンドポイントとイベント名の型安全なパターンを可能にします。

typescript
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world';  // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';

Uppercase と Lowercase

組み込みの固有型が文字列リテラル型を変換します。テンプレートリテラルと組み合わせて型安全なイベント名と定数を生成します。

typescript
type Upper = Uppercase<'hello'>;  // 'HELLO'
type Lower = Lowercase<'WORLD'>;  // 'world'
type Cap = Capitalize<'foo'>;     // 'Foo'
type EventName = `on${Capitalize<'click'>}`;  // 'onClick'

キーリマッピング

キーリマッピング(as 句)がマップ型中にキーを変換します。プロパティ名から getter/setter 名を生成します。インターフェースから型安全な API を作成します。

typescript
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

文字列パターンマッチング

infer を伴うテンプレートリテラル型はコンパイル時に文字列を解析できます。Split は文字列をタプルに分割します。型安全な文字列操作を可能にします。

typescript
type Split<S extends string, D extends string> =
    S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>;  // ['a', 'b', 'c']

イベントシステムの型付け

ジェネリクスを伴うテンプレートリテラル型で完全に型安全なイベントシステムを作成します。イベント名がペイロード型を決定します。on と emit が一致する型を強制します。

typescript
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
    on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
    emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x));  // Typed!
26

infer キーワード

戻り値型の抽出

infer は条件型内で型変数を宣言します。特定の位置の型をキャプチャします。ReturnType が組み込みの等価物です。

typescript
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>;  // { name: string; age: number; }

Promise 型の抽出

infer が Promise の内部型を抽出します。DeepUnwrap がネストされた Promise を再帰的にアンラップします。モダンな TypeScript の組み込み Awaited<T> がこれを行います。

typescript
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>;  // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>;  // boolean

配列要素の抽出

infer E が配列の要素型をキャプチャします。タプルの場合、infer は特定の位置をキャプチャできます。ジェネリックコレクションの操作に有用です。

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>;  // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>;  // string

関数パラメータの抽出

infer P が関数のパラメータタプルをキャプチャします。Parameters が組み込みの等価物です。型を保持しながら関数をラップするのに有用です。

typescript
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>;  // [string, number]

複数 infer

複数の infer 変数で型の異なる部分を同時にキャプチャできます。単一の条件で複雑な型変換を可能にします。

typescript
type FirstLast<T extends any[]> =
    T extends [infer First, ...any[], infer Last]
        ? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>;  // { first: 1; last: 4 }
27

変性

共変性

共変性は Dog[] を Animal[] に代入できるようにします。TypeScript 配列は共変ですが、これは安全ではありません:Animal を Dog[] に push すると配列が破損します。

typescript
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs;  // OK (covariant)
// animals.push(new Animal());  // Unsafe at runtime

反変性

反変性は Dog を受け入れる関数が Animal を受け入れる関数が期待される場所で使用できることを意味します。Dog ハンドラは Dog である任意の Animal を処理できるため安全です。

typescript
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler;  // OK with strictFunctionTypes

双変性

メソッド構文は双変です。関数プロパティ構文は strictFunctionTypes で反変です。メソッドは OO 互換性のために双変です。

typescript
interface IFace {
    method(x: Animal): void;  // bivariant
    fn: (x: Animal) => void;  // contravariant
}
class Impl implements IFace {
    method(x: Dog) {}  // OK (bivariant)
    fn = (x: Dog) => {}  // Error (contravariant)
}

in/out 変性

TypeScript 4.7+ は明示的な変性注釈をサポートします。in は反変(コンシューマ)、out は共変(プロデューサ)、in out は不変(両方)をマークします。

typescript
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }

不変型

不変型は正確な型一致が必要です。型が入力と出力の両方の位置に出現する場合、不変になります。Box<Dog> は Box<Animal> に代入できません。

typescript
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox;  // Error: invariant
28

ビルダーパターン

流暢なビルダー

ビルダーパターンは複雑なオブジェクトを段階的に構築します。各メソッドがチェーン用に this を返します。SQL クエリ、HTTP リクエスト、設定に有用です。

typescript
class QueryBuilder {
    private parts: string[] = [];
    select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
    from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
    where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
    build(): string { return this.parts.join(' '); }
}

型安全ビルダー

型安全ビルダーは条件型で必須フィールドを強制します。build() は hasName が true の場合のみ Person を返します。コンパイル時に欠落フィールドをキャッチします。

typescript
interface State { hasName: boolean; }
class Builder<S extends State> {
    name(n: string): Builder<S & { hasName: true }> { return this as any; }
    build(): S extends { hasName: true } ? Person : never { return {} as any; }
}

不変ビルダー

不変ビルダーは各変更で新しいインスタンスを作成します。型システムが交差型を通じて追加された全キーを追跡します。各 set が新しいビルダー型を返します。

typescript
class ImmutableBuilder<T extends object> {
    constructor(private data: T) {}
    set<K extends string, V>(key: K, value: V):
        ImmutableBuilder<T & { [P in K]: V }> {
        return new ImmutableBuilder({ ...this.data, [key]: value });
    }
    build(): T { return this.data; }
}

ディレクターパターン

ディレクターは一般的な構築シーケンスをカプセル化します。ビルダーを使用して標準プロダクトを作成します。異なるディレクターが異なるバリエーションを生成します。

typescript
class HTMLBuilder {
    private html = '';
    addTag(tag: string, content: string): this {
        this.html += `<${tag}>${content}</${tag}>`; return this;
    }
    build(): string { return this.html; }
}
class Director {
    buildPage(title: string, body: string): string {
        return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
    }
}

ステップビルダー

ステップビルダーは型システムを通じてメソッド呼び出しの特定の順序を強制します。各ステップが次のメソッドのみ利用可能な異なる型を返します。

typescript
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
    constructor(private name: string) {}
    age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
    constructor(private name: string, private age: number) {}
    build() { return { name: this.name, age: this.age }; }
}
29

TypeScript でのテスト

TypeScript での Jest

TypeScript テストには ts-jest または @swc/jest を使用します。describe が関連テストをグループ化し、it がテストケースを定義します。expect が toBe、toEqual のようなマッチャでアサーションを作成します。

typescript
import { sum } from './sum';
describe('sum', () => {
    it('adds two numbers', () => {
        expect(sum(1, 2)).toBe(3);
    });
    it('handles negatives', () => {
        expect(sum(-1, -2)).toBe(-3);
    });
});

型テスト

expectTypeOf はコンパイル時に型をテストします。戻り値型、パラメータ型、解決された Promise 型を検証します。型が間違っているとビルドが失敗します。

typescript
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
    const fn = {} as unknown as GetUser;
    expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
    expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});

型付きモック

jest.Mocked<T> がインターフェースから型付きモックを作成します。jest.fn() が型付き戻り値でモック関数を作成します。モックは完全に型付きです。

typescript
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
    find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });

テストセットアップとティアダウン

beforeAll は全テスト前に1回実行、afterAll は後に1回実行。beforeEach は各テスト前に、afterEach は後に実行。セットアップとクリーンアップに使用します。

typescript
describe('Database', () => {
    beforeAll(async () => { db = createDatabase(); await db.connect(); });
    afterAll(async () => { await db.disconnect(); });
    beforeEach(async () => { await db.clear(); });
    afterEach(() => { jest.restoreAllMocks(); });
});

プロパティベーステスト

プロパティベーステストはランダム入力を生成して不変条件をテストします。fc.assert がプロパティを複数回実行します。例ベーステストが見逃すエッジケースをキャッチします。

typescript
import { fc } from 'fast-check';
describe('sort', () => {
    it('preserves length', () => {
        fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
            return [...arr].sort().length === arr.length;
        }));
    });
});
30

よくある落とし穴

any vs unknown

any は型チェックを無効にし、バグを隠します。unknown は型安全です:使用前にナローイングする必要があります。信頼できないソース(API、JSON.parse)には unknown を使用してください。

typescript
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
    if (typeof data === 'object' && data !== null && 'foo' in data)
        return (data as any).foo;
}

余剰プロパティチェック

TypeScript は直接代入されたオブジェクトリテラルの余剰プロパティのみチェックします。変数経由の場合、チェックはスキップされます。より厳格なランタイムバリデーションには zod を使用してください。

typescript
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true };  // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data;  // OK

列挙型 vs ユニオン

列挙型は逆マッピング付きのランタイムオブジェクトを作成します。ユニオン型はランタイムゼロでツリーシェイク可能です。新規コードにはユニオン型を優先してください。

typescript
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }

構造的型付け

TypeScript は構造的型付けを使用します:形状が一致すれば型は互換です。Admin は User に代入可能です。これは論理的バグを引き起こす可能性があります。ブランド型で名目的区別を追加します。

typescript
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin;  // OK (structural)

型アサーションの危険性

型アサーション(as)はランタイムチェックなしで TypeScript をオーバーライドします。外部データにはランタイムバリデーション(zod、io-ts)を使用してください。safeParse はスローせずに結果を返します。

typescript
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.