C
JavaScript/TypeScript/Lesson 21

TypeScript 最小基礎 — 型エラーの読み方・any の回避

45分·theory

TypeScript 最小基礎 — 型エラーの読み方・any の回避

🎯 このレッスンを読み終えたら

このレッスンをすべて読み終えると、以下の 3 つを自信を持ってできるようになります。

  • ✅ any を unknown に置き換えて安全性を回復する
  • ✅ interface vs type・ユニオン・オプショナル・never を理解する
  • ✅ Utility Types 5 つ (Partial・Pick・Omit・Record・Required) を使いこなす

学習目標をチェックリストとして手元に置き、すべて答えられるようになったらレッスンを閉じてください。

⚠️ tsconfig の前提条件 — strict モード基準

すべての例は "strict": true を前提としています

jsonc
{
  "compilerOptions": {
    "strict": true,            // 以下 7 つを一括で有効化
    // strictNullChecks: null/undefined を別の型として扱う
    // noImplicitAny: 推論失敗時に any を自動付与しない
    // strictFunctionTypes
    // strictBindCallApply
    // strictPropertyInitialization
    // noImplicitThis
    // useUnknownInCatchVariables
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

strict がオフの場合 — 別の言語になります

  • nullすべての型に代入可能string 型変数に null が入ってもエラーにならない
  • noImplicitAny がオフだと function f(x)x が暗黙的に any になる → 型検査が無効化される
  • unknown / never のナローイング動作が緩くなる

確認方法

bash
npx tsc --showConfig | grep strict

ルール: 新規プロジェクトは strict: true がデフォルト (Vite・Next.js・CRA すべて)。既存プロジェクトで練習する前に必ず確認してください。オフになっていると「なぜ本の通りにならないのか?」の 99% の原因がこれです。

TypeScript がトークンを節約する理由

事実 — 2026 年の新規プロジェクトの 90% は TypeScript を使用

Cursor、v0.dev、Claude Code、GitHub Copilot — すべて TypeScript を優先的に生成します。JS で始めても、いつか .ts ファイルが追加されます。

トークン節約のメカニズム — 型情報がコンテキストになる

typescript
interface User {
    id: number;
    email: string;
    role: 'admin' | 'user';
}

function sendEmail(user: User, template: string): Promise<void> { ... }

AI に「sendEmail(currentUser, welcome) を呼び出すコードを書いて」と頼むと:

  • JS の場合 → user が何か・どんなフィールドがあるかを改めて説明する必要がある
  • TS の場合 → AI が interface を見るだけで自動推論。追加説明は不要

型定義そのものが AI のコンテキストになります。追加プロンプト = 追加トークン。TS で減らせます。

基本型 8 種類

typescript
let n: number = 42;
let s: string = 'hello';
let b: boolean = true;
let a: number[] = [1, 2, 3];
let tup: [string, number] = ['A', 30];     // タプル
let anyVal: any = '何でも';                // ❌ 使用を避ける
let unknownVal: unknown = JSON.parse(raw); // ✅ any の代わりに
let voidVal: void = undefined;             // 戻り値なし

any が危険な理由

typescript
const x: any = 'hello';
x.toFixed(2);   // ランタイムエラー — コンパイラが検査しない

any は TypeScript の安全性をすべてオフにします。AI が any を使っていたら unknown に変えてくださいとお願いしましょう。

unknown — 安全な any

typescript
const raw = '"hello"';
const x: unknown = JSON.parse(raw);

// ❌ そのまま使うとコンパイルエラー — 「unknown なのに toUpperCase を呼ぼうとしている?」
// x.toUpperCase();   // TS error: 'x' is of type 'unknown'

// ✅ 先に型を確認 (ナローイング) してから使う
if (typeof x === 'string') {
    console.log(x.toUpperCase());   // "HELLO"   ← このブロック内では x: string に絞られる
}

// 💡 any: すべての検査をオフ → 危険
//    unknown: 確認前は使えない → 安全

使用前に型確認を強制。API レスポンスの解析・外部入力の処理に標準的に使います。

interface vs type・ユニオン・オプショナル

interface vs type — どちらをいつ使うか

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

type Status = 'active' | 'inactive';
type Point = { x: number; y: number };
interfacetype
オブジェクトの形
ユニオン/インターセクション
extends✅ (&)
宣言マージ

ルール: オブジェクトの形には interfaceそれ以外 (ユニオン・タプル・マッピング) には type。チームの規約が「すべて type」という場合も多い。

ユニオン |複数の型のうちの一つ

typescript
type ID = number | string;
function find(id: ID) { ... }

find(123);       // OK
find('abc');     // OK
find(true);      // ❌

リテラルユニオンが最もよく使われます:

typescript
type Role = 'admin' | 'user' | 'guest';
// 'admin'、'user'、'guest' の 3 つのうちいずれか一つのみ許可

enum の代わりにリテラルユニオンが現在の標準です。

オプショナル ?

typescript
interface CreateUserInput {
    email: string;
    name: string;
    age?: number;       // あってもなくてもよい
    role?: 'admin' | 'user';
}

function create(input: CreateUserInput) {
    const age = input.age ?? 0;
    // ...
}

? が付くと undefined も許容されます。関数の引数や DTO によく使われます。

ジェネリクス <T> — 基本形

typescript
function firstItem<T>(arr: T[]): T | undefined {
    return arr[0];
}

const a = firstItem([1, 2, 3]);     // T = number → number | undefined
const b = firstItem(['a', 'b']);    // T = string → string | undefined

console.log(a);   // 1     ← 型: number | undefined
console.log(b);   // 'a'   ← 型: string | undefined

// 💡 呼び出すたびに T が「引数の型」として自動決定される
//    → 一つの関数でどんな配列の型でも安全に処理できる

「型を変数のように」 — 呼び出し時に決まります。ライブラリ (React Hook、Array メソッド) で広く使われています。

never — 絶対に到達しない型

never の正体

すべての型のサブタイプ。「存在し得ない値」を表します。絶対に正常終了しない関数 (throw または無限ループ) の戻り値型も never です:

typescript
function fail(msg: string): never {
    throw new Error(msg);   // 正常終了しない
}

function loop(): never {
    while (true) { /* 永遠に */ }
}

Exhaustive Check — switch の漏れをコンパイル時に検出

typescript
type Shape = 'circle' | 'square' | 'triangle';

function area(s: Shape): number {
    switch (s) {
        case 'circle':   return Math.PI;
        case 'square':   return 1;
        case 'triangle': return 0.5;
        default:
            // 上の case が Shape のすべてのメンバーを処理していれば、s の型は never になる
            const _check: never = s;
            return _check;
    }
}

Shape'star' を追加すると — switch がそれを処理していないので s の型が 'star' に絞られる → never 型の変数に代入できない → コンパイルエラー。「新しいケースを追加したなら、ここも処理してください」というコンパイラからの強制通知です。

Java の sealed クラス、Rust の enum マッチングと同じ思想の TypeScript 版です。

never vs void — 面接の定番問題

voidnever
意味戻り値なし絶対に戻らない
関数の終了正常終了 (return undefined)throw / 無限ループ
変数への代入let x: void = undefined は可能どんな値も代入不可
typescript
function logOnly(msg: string): void { console.log(msg); }      // 正常終了
function fail(msg: string): never  { throw new Error(msg); }    // throw

const a: void = undefined;     // OK
const b: never = ???;          // どんな値を入れてもコンパイルエラー

実務での活用 — discriminated union の安全網

typescript
type Event =
    | { kind: 'click'; x: number; y: number }
    | { kind: 'scroll'; offset: number }
    | { kind: 'keypress'; key: string };

function handle(e: Event): string {
    switch (e.kind) {
        case 'click':    return `clicked at ${e.x},${e.y}`;
        case 'scroll':   return `scrolled ${e.offset}`;
        case 'keypress': return `pressed ${e.key}`;
        default:
            const _: never = e;   // 新しい event 追加時にここで検出される
            throw new Error('unhandled event');
    }
}

TypeScript コードにこのパターンが書かれているすべての場所 — discriminated union + never デフォルトチェック。これを知っていれば、面接で「TypeScript が JavaScript より優れている点」の答えが確固たるものになります。

Utility Types — 面接頻出の 5 つ

TypeScript 組み込みの型変換器

既存の型を加工して新しい型を作ります。この 5 つを知っていれば実務の 99% をカバーできます。

typescript
interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user';
}

1. Partial<T> — すべてのフィールドをオプションに

typescript
type UpdateUser = Partial<User>;
// 結果: { id?: number; name?: string; email?: string; role?: ... }

// PATCH API の標準 — 一部のフィールドだけを送るリクエストボディ
function patchUser(id: number, patch: Partial<User>) {
    return fetch(`/api/users/${id}`, {
        method: 'PATCH',
        body: JSON.stringify(patch)
    });
}

patchUser(1, { name: 'Alice' });          // OK
patchUser(1, { name: 'A', email: 'a' });  // OK
patchUser(1, {});                          // OK

2. Required<T> — すべてのフィールドを必須に (Partial の逆)

typescript
interface Draft { title?: string; body?: string; }

type Published = Required<Draft>;
// { title: string; body: string }

// 公開前のバリデーションゲート
function publish(d: Required<Draft>) { /* title と body の両方があることが保証される */ }

3. Pick<T, K> — 一部のフィールドだけを選択

typescript
type UserCard = Pick<User, 'id' | 'name'>;
// { id: number; name: string }

function renderCard(u: UserCard) {
    return `<div>${u.id}: ${u.name}</div>`;
}

4. Omit<T, K> — 一部のフィールドを除外

typescript
type PublicUser = Omit<User, 'email' | 'role'>;
// { id: number; name: string }

// 機密情報の除去 (API レスポンスからパスワード・メールを削除) の標準パターン
function toPublic(u: User): PublicUser {
    const { email, role, ...pub } = u;
    return pub;
}

5. Record<K, V> — キーと値のマップ

typescript
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
// { admin: string[]; user: string[]; guest: string[] }

const perms: RolePermissions = {
    admin: ['read', 'write', 'delete'],
    user:  ['read', 'write'],
    guest: ['read']
};

enum の代替 — キーがユニオン型なら、コンパイラがすべてのキーが揃っているか検査します。欠けているとエラーになります。

面接のポイント — この 3 つのパターンが実務で毎日登場する

  • Partial → PATCH API DTO (Partial<User>)
  • Omit → 機密フィールドの削除 (Omit<User, 'password' | 'tokenSecret'>)
  • Record → 権限・言語・状態のマップ (Record<Role, Permission[]>)

その他の Utility Types — 知っていると役立つ

typescript
type A = ReturnType<typeof fn>;       // 関数の戻り値型を抽出
type B = Parameters<typeof fn>;       // 関数のパラメータタプル
type C = Awaited<Promise<string>>;    // Promise をアンラップ → string
type D = NonNullable<string | null>;  // null/undefined を除去
type E = Readonly<User>;              // すべてのフィールドを readonly に

ReturnType / Awaited も面接でよく出ます。「この関数の戻り値型を他の場所でどう再利用しますか?」という問いへの答えが ReturnType です。

型エラーの読み方・型アサーション・React との連携

エラーメッセージをじっくり読んでください

code
Type '{ id: number; }' is not assignable to type 'User'.
  Property 'email' is missing in type '{ id: number; }' but required in type 'User'.

本当の原因は下の行にあります。「email がない」— 渡す側が受け取る側の型を満たしていない

型アサーション as多用すると危険

typescript
const el = document.getElementById('app') as HTMLDivElement;

「信じて、これは HTMLDivElement だ」 — TypeScript の検査を手動で無効化します。

乱用禁止。as だらけのコードは TypeScript を使わないのと同じです。ナローイング (if (typeof x === ...)) や型ガードを先に検討してください。

型ガード

typescript
function isUser(x: unknown): x is User {
    return typeof x === 'object' && x !== null && 'email' in x;
}

if (isUser(data)) {
    console.log(data.email);   // ✅ ナローイング完了
}

x is User が戻り値の型 — 呼び出し元で TypeScript がナローイングを適用します。

React コンポーネントの型

tsx
interface ButtonProps {
    label: string;
    onClick: () => void;
    variant?: 'primary' | 'secondary';
}

function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
    return <button onClick={onClick} className={variant}>{label}</button>;
}

props インターフェースを分離して定義するのが標準です。

useState の型推論

tsx
const [count, setCount] = useState(0);             // number として推論される
const [user, setUser] = useState<User | null>(null);  // 明示的に指定

null の可能性がある場合は明示する — 推論が null だけを見て null 型に絞られてしまうという落とし穴があります。

API レスポンスの型

typescript
async function fetchUser(id: number): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error('failed');
    return res.json();   // Promise<any> を Promise<User> としてアサート
}

戻り値の型を明示すると、呼び出し元で自動的にナローイングが適用されます。

🤖 AI へのお願いの仕方

  • 「この関数に TypeScript の型を付けてください。any は使わず、unknown か明示的な型で」
  • 「このエラーメッセージを日本語で説明してください」 (そのまま貼り付ければ AI が解説してくれます)
  • 「この as アサーションを型ガードに書き換えてください」

TypeScript の 5 つ (interface・ユニオン・オプショナル・ナローイング・ジェネリクス) だけ知っていれば、バイブコーディングの 90% が解決できます。

`as` vs `as const` — まったく異なる 2 つのツール

as — 型システムの回避 (危険)

typescript
const role = 'admin' as 'admin';   // 強制アサーション
const x = JSON.parse(raw) as User;  // 「信じて、これは User だ」

コンパイラの検査を手動でオフにします。間違ったアサーションをするとランタイムで爆発します

typescript
// ❌ 危険な as
const data = '{ malformed }' as User;
data.email.toLowerCase();   // ランタイム TypeError

as const — リテラル型を固定 (安全な代替手段)

typescript
const ROLES = ['admin', 'user', 'guest'] as const;
// 型: readonly ['admin', 'user', 'guest']  (絞り込まれたリテラル)

// as const がない場合:
const ROLES2 = ['admin', 'user', 'guest'];
// 型: string[]   (「文字列の配列」— 情報が失われる)

配列 → ユニオン型の生成パターン — as const の真の価値

typescript
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number];
// type Role = 'admin' | 'user' | 'guest'

値と型が常に同期されますROLES'staff' を追加すると Role も自動的に 'admin' | 'user' | 'guest' | 'staff' になります。通常の enum の限界をエレガントに克服します。

オブジェクトの凍結 — 設定・定数のまとめ

typescript
const CONFIG = {
    endpoint: 'https://api.example.com',
    timeout: 3000,
    retries: 3
} as const;

// CONFIG.timeout の型: 3000 (number ではなく — まさにそのリテラル)
// CONFIG.endpoint の型: 'https://api.example.com'
// CONFIG のすべてのプロパティが readonly

CONFIG.timeout = 5000;   // ❌ コンパイルエラー — readonly

実務の例 — Discriminated Union の生成

typescript
const EVENT_TYPES = ['click', 'scroll', 'keypress'] as const;
type EventType = typeof EVENT_TYPES[number];
// 'click' | 'scroll' | 'keypress'

interface AppEvent {
    type: EventType;
    timestamp: number;
}

function handle(e: AppEvent) {
    if (e.type === 'click') { /* ナロー */ }
}

一言まとめ

  • asコンパイラを騙して型を強制します。ほとんど使わないでください。
  • as const値から正確なリテラル型を抽出します。安全です。積極的に使いましょう。

⚡ 実際に試してみよう — TS の概念 (ランタイム型検証)

サンドボックスが TypeScript コンパイラに対応していないため、*コンパイル時の検査の代わりに、ランタイムの `typeof` / `instanceof` を使って* TypeScript のコアコンセプト (unknown・ナローイング・型ガード) をデモします。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可
TypeScript 最小基礎 — 型エラーの読み方・any の回避 - JavaScript