TypeScript型の基礎完全ガイド〜プリミティブ・オブジェクト・配列・ユニオン・リテラル【2026年版】〜

TypeScriptを書き始めたばかりの頃、誰もが一度はぶつかる壁が「型の書き方が分からない」「anyだらけになってしまう」という問題だ。本記事は、20〜40代の現役Webエンジニアを対象に、TypeScript 5.x時代における型の基礎を完全網羅する。プリミティブ・配列・オブジェクト・タプル・ユニオン・リテラル・ジェネリクス入門まで、コピペで動くサンプルコードを40個以上掲載し、JavaScriptからの移行・any地獄からの脱出・型安全なリファクタリングの実例を「Before / After」形式で徹底解説する。

既存の「TypeScript完全実践ガイド」が総合カタログだとすれば、本記事は型の基礎に集中したHub記事として位置づける。読み終える頃には、型注釈の必要な箇所・不要な箇所を判断でき、ユニオン型でnarrowingを書けるレベルになる。

  1. 環境準備:tsconfig.jsonの推奨設定
    1. 最小推奨tsconfig
    2. strict配下に含まれるフラグの内訳
    3. noUncheckedIndexedAccessは入れる派
  2. プリミティブ型:7種類を完全把握する
    1. string / number / boolean
    2. 大文字始まりの型は使わない
    3. null と undefined の使い分け
    4. bigintとsymbol
  3. 配列とタプル:似て非なる型
    1. 配列の2種類の書き方
    2. タプル(固定長・各要素別の型)
    3. readonly tupleとas const
  4. オブジェクト型:typeとinterfaceの使い分け
    1. type alias による定義
    2. interface による定義
    3. 使い分けの判断基準
    4. オプショナル ・readonly・index signature
    5. interface の extends
    6. type の intersection
  5. ユニオン型・リテラル型・narrowing
    1. ユニオン型の基本
    2. リテラル型(値そのものを型に)
    3. typeofによるnarrowing
    4. in 演算子によるnarrowing
    5. instanceof によるnarrowing
    6. discriminated union(判別ユニオン)
    7. 網羅性チェック:never型の活用
  6. enum vs as const:なぜ as const 推奨か
    1. 従来のenum(非推奨寄り)
    2. as const オブジェクト方式(推奨)
    3. enum vs as const 比較表
  7. typeof と keyof:型から型を作る基本
    1. typeof で値から型を取り出す
    2. keyof でプロパティ名のユニオンを取り出す
    3. typeof + keyof の合わせ技
  8. 関数の型:シグネチャ・オーバーロード・thisまで
    1. 関数シグネチャの書き方
    2. オプショナル引数・デフォルト引数・rest引数
    3. 関数オーバーロード
    4. thisの型注釈
    5. 戻り値の型推論 vs 明示
  9. any / unknown / never / void:似て非なる4型
    1. any:型チェックを完全に切る最終手段
    2. unknown:any の安全な代替
    3. never:値が存在しない型
    4. void:戻り値を使わない宣言
    5. 4型の比較表
  10. as const と satisfies:TS 4.9以降の必修
    1. as const アサーションのおさらい
    2. satisfies 演算子(TS 4.9+)の意義
    3. satisfies の現場ユースケース
  11. import type と verbatimModuleSyntax
    1. 型のみインポート
    2. verbatimModuleSyntax の意味
    3. export type
  12. 実践:any地獄からの脱出リファクタリング
    1. Before:JavaScriptそのまま移植
    2. After:型を入れて安全に
    3. 段階移行のコツ
  13. 型注釈は「どこに書くか」が9割
    1. 書くべき箇所:境界線(API・関数の入口出口)
    2. 書かなくていい箇所:ローカル変数
    3. 判断フロー
  14. React + TypeScript の最小サンプル
    1. Props の型定義
    2. useState の型推論と明示
    3. useReducer + discriminated union
  15. よくあるエラーメッセージと解消法
    1. Object is possibly ‘undefined’
    2. Argument of type ‘X’ is not assignable to parameter of type ‘Y’
    3. Type ‘string’ is not assignable to type ‘never’
  16. FAQ
    1. Q. JavaScriptしか書いたことがありません。TypeScriptは難しいですか?
    2. Q. type と interface はどちらを使えばいいですか?
    3. Q. any を絶対に使ってはいけませんか?
    4. Q. enum は使うべきですか?
    5. Q. tsconfigで何を最初にONにすべきですか?
    6. Q. import type と import の通常形はどう違いますか?
    7. Q. 学習を加速したいです。スクールでまとめて学ぶ価値はありますか?
  17. まとめ

環境準備:tsconfig.jsonの推奨設定

型の話に入る前に、TypeScript 5.x 推奨の tsconfig.json を確認しておく。strict: true が無効だと、本記事の例の半分は警告すら出ない。

最小推奨tsconfig

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

strict配下に含まれるフラグの内訳

フラグ役割外すとどうなるか
noImplicitAny暗黙的anyを禁止型を書き忘れた箇所が全てanyに
strictNullChecksnull/undefinedを別物として扱うnullアクセス事故が型でなくランタイムで露呈
strictFunctionTypes関数引数を反変として扱うcallback型の代入が緩くなる
strictBindCallApplybind/call/applyを型検査引数ミスマッチが通る
alwaysStrict“use strict”自動付与暗黙的グローバル化リスク

noUncheckedIndexedAccessは入れる派

// noUncheckedIndexedAccess: true
const arr = ["a", "b", "c"];
const first = arr[0];
//    ^? const first: string | undefined

// false の場合は string になる(範囲外アクセスが型で見えない)

このフラグはTypeScriptの「嘘をつかない型」運用の入り口だ。配列の添字アクセスは厳密にはT | undefinedだから、それを型に反映してくれる。

プリミティブ型:7種類を完全把握する

TypeScriptのプリミティブ型は string / number / boolean / null / undefined / bigint / symbol の7種類。StringやNumberとは別物なので注意。

string / number / boolean

let userName: string = "Taro";
let age: number = 30;
let isActive: boolean = true;

// テンプレートリテラルもstring
const greet: string = `Hello, ${userName}`;

// 16進・2進・指数表記すべてnumber
const hex: number = 0xff;
const bin: number = 0b1010;
const exp: number = 1.5e3;

大文字始まりの型は使わない

// Before(ダメ)
let name: String = "Taro";   // String型(オブジェクトラッパー)
let age: Number = 30;        // Number型

// After(正しい)
let userName: string = "Taro";
let userAge: number = 30;

// String型はメソッドが効かない箇所がある
const s: String = "hello";
// s.toUpperCase() は動くが、関数引数で string を要求されると渡せない

null と undefined の使い分け

// undefined: 値が「まだ無い」
let assigned: string | undefined;
assigned = "later";

// null: 値が「明示的に無い」
type ApiUser = {
  id: number;
  nickname: string | null; // DBのNULLを表現
};

// strictNullChecksがONなら、両者は別の型
function f(x: string) { /* ... */ }
let n: null = null;
// f(n); // Error: 'null' is not assignable to 'string'

bigintとsymbol

// bigint: 任意精度整数(末尾n)
const big: bigint = 9007199254740993n;
const sum: bigint = big + 1n; // numberと混ぜられない

// symbol: 一意な識別子
const SECRET: symbol = Symbol("secret");
const obj = { [SECRET]: "value" };

配列とタプル:似て非なる型

配列の2種類の書き方

// 短縮形(推奨)
const names: string[] = ["Alice", "Bob"];

// ジェネリック形(混在型で読みやすい)
const ids: Array<number | string> = [1, "u_2", 3];

// readonly配列(push/popできない)
const nums: readonly number[] = [1, 2, 3];
// nums.push(4); // Error

タプル(固定長・各要素別の型)

// タプル
type Point = [number, number];
const p: Point = [10, 20];

// ラベル付きタプル(TS 4.0以降)
type RGB = [r: number, g: number, b: number];
const red: RGB = [255, 0, 0];

// React useState 風の戻り値
type UseToggle = [value: boolean, toggle: () => void];

// 可変長タプル(rest)
type WithFirst<T> = [first: T, ...rest: T[]];
const list: WithFirst<string> = ["head", "tail1", "tail2"];

readonly tupleとas const

// readonly tuple
const point: readonly [number, number] = [1, 2];

// as const で全要素 literal & readonly
const route = ["GET", "/users", 200] as const;
//    ^? readonly ["GET", "/users", 200]

// 関数の戻り値を as const にすると Hooks の型推論が綺麗
function useToggle(initial = false) {
  const [v, setV] = useState(initial);
  const toggle = () => setV(prev => !prev);
  return [v, toggle] as const; // [boolean, () => void] に推論
}

オブジェクト型:typeとinterfaceの使い分け

type alias による定義

type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

const u: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  isActive: true,
};

interface による定義

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

// 同名で拡張(declaration merging)
interface User {
  isActive: boolean;
}

// 結果:User = { id; name; email; isActive }
const u: User = { id: 1, name: "Alice", email: "x", isActive: true };

使い分けの判断基準

状況推奨理由
外部APIレスポンス型typeユニオン・交差・mapped typeが書きやすい
クラスのcontractinterfaceimplementsで使う前提
拡張ポイント(ライブラリ作者)interfacedeclaration mergingでユーザーが拡張可能
ユニオン型を含むtypeinterfaceはユニオンを直接持てない
関数シグネチャ集合どちらでも可readability優先

オプショナル ・readonly・index signature

type Article = {
  id: number;
  title: string;
  subtitle?: string;        // オプショナル
  readonly slug: string;    // 書き換え不可
  [meta: string]: unknown;  // index signature
};

const a: Article = {
  id: 1,
  title: "Hello",
  slug: "hello",
  publishedAt: "2026-05-26",
};
// a.slug = "x"; // Error: Cannot assign to 'slug'

interface の extends

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// 複数継承も可能
interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

interface Post extends Timestamps {
  id: number;
  title: string;
}

type の intersection

type WithId = { id: number };
type WithName = { name: string };

// 交差型(intersection)
type Entity = WithId & WithName;

const e: Entity = { id: 1, name: "Alice" };

// 矛盾するプロパティはnever
type A = { kind: "a" };
type B = { kind: "b" };
type AB = A & B;
//   ^? { kind: never } 事実上使えない

ユニオン型・リテラル型・narrowing

ユニオン型の基本

// 値の取り得る型を | で並べる
type ID = number | string;

const a: ID = 1;
const b: ID = "user_1";

function format(id: ID): string {
  // id.toUpperCase(); // Error: numberにはtoUpperCaseがない
  return String(id);  // 共通メソッドのみ使える
}

リテラル型(値そのものを型に)

// 文字列リテラル型
type Direction = "north" | "south" | "east" | "west";

function move(dir: Direction) {
  // ...
}
move("north");     // OK
// move("up");     // Error

// 数値リテラル型
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

// boolean リテラル型
type LoadingState = { loading: true } | { loading: false; data: string };

typeofによるnarrowing

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // ここでは string に絞り込まれる
  }
  // ここでは number
  return value.toFixed(2);
}

in 演算子によるnarrowing

type Cat = { name: string; purr: () => void };
type Dog = { name: string; bark: () => void };

function speak(pet: Cat | Dog) {
  if ("purr" in pet) {
    pet.purr(); // Cat に絞り込まれる
  } else {
    pet.bark(); // Dog
  }
}

instanceof によるnarrowing

class AppError extends Error {
  constructor(public code: number, message: string) {
    super(message);
  }
}

function handle(e: unknown) {
  if (e instanceof AppError) {
    console.error(`[${e.code}] ${e.message}`);
  } else if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error("Unknown error", e);
  }
}

discriminated union(判別ユニオン)

// 共通の判別子 kind を持たせる
type Result<T> =
  | { kind: "ok"; value: T }
  | { kind: "error"; message: string };

function unwrap<T>(r: Result<T>): T {
  if (r.kind === "ok") {
    return r.value;       // T
  }
  throw new Error(r.message); // string
}

// React の fetch ステータスにも応用
type FetchState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

網羅性チェック:never型の活用

type Status = "idle" | "loading" | "success" | "error";

function describe(s: Status): string {
  switch (s) {
    case "idle": return "待機中";
    case "loading": return "読込中";
    case "success": return "成功";
    case "error": return "失敗";
    default: {
      const _exhaustive: never = s; // ← ここでケース漏れがコンパイルエラー
      return _exhaustive;
    }
  }
}
// Statusに "cancel" を追加すると、default節でコンパイルエラーが出る

enum vs as const:なぜ as const 推奨か

従来のenum(非推奨寄り)

enum Role {
  Admin,
  Editor,
  Viewer,
}

// 内部値が 0,1,2(意図しない数値リーク)
console.log(Role.Admin); // 0

// string enum なら値は明示
enum Status {
  Idle = "idle",
  Loading = "loading",
}

as const オブジェクト方式(推奨)

// Before
enum Role { Admin, Editor, Viewer }

// After
const Role = {
  Admin: "admin",
  Editor: "editor",
  Viewer: "viewer",
} as const;

type Role = typeof Role[keyof typeof Role];
//   ^? "admin" | "editor" | "viewer"

function setRole(r: Role) { /* ... */ }
setRole(Role.Admin);
setRole("editor"); // 文字列リテラルでも渡せる(柔軟)

enum vs as const 比較表

項目enumas const オブジェクト
ランタイム出力独自オブジェクトを生成そのままJSオブジェクト
Tree-shakingconst enum以外は効きにくい効く
文字列リテラル代入不可
declaration mergingありなし
–isolatedModules相性const enumと相性悪い無問題
初学者の理解独自概念で混乱しがちJSの延長で理解しやすい

typeof と keyof:型から型を作る基本

typeof で値から型を取り出す

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retry: 3,
};

type Config = typeof config;
//   ^? { apiUrl: string; timeout: number; retry: number }

// JSON設定ファイルを単一の真実の源(SSOT)にできる

keyof でプロパティ名のユニオンを取り出す

type User = {
  id: number;
  name: string;
  email: string;
};

type UserKey = keyof User;
//   ^? "id" | "name" | "email"

function getProp<K extends keyof User>(u: User, key: K): User[K] {
  return u[key];
}

const u: User = { id: 1, name: "Alice", email: "x" };
const id = getProp(u, "id");     // number
const name = getProp(u, "name"); // string
// const x = getProp(u, "age");  // Error

typeof + keyof の合わせ技

const ROUTES = {
  home: "/",
  posts: "/posts",
  postDetail: "/posts/:id",
  about: "/about",
} as const;

type RouteName = keyof typeof ROUTES;
//   ^? "home" | "posts" | "postDetail" | "about"

type RoutePath = typeof ROUTES[RouteName];
//   ^? "/" | "/posts" | "/posts/:id" | "/about"

function go(name: RouteName) {
  window.location.href = ROUTES[name];
}

関数の型:シグネチャ・オーバーロード・thisまで

関数シグネチャの書き方

// 関数式に型注釈
const add: (a: number, b: number) => number = (a, b) => a + b;

// type alias で再利用
type BinaryOp = (a: number, b: number) => number;
const sub: BinaryOp = (a, b) => a - b;

// interface でも関数型を表現可能
interface Reducer<S, A> {
  (state: S, action: A): S;
}

オプショナル引数・デフォルト引数・rest引数

function greet(name: string, prefix?: string): string {
  return `${prefix ?? "Mr."} ${name}`;
}

function withDefault(timeout: number = 5000): void {
  // timeout は number
}

function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6

関数オーバーロード

// 戻り値が引数の型に依存するケース
function parse(input: string): string[];
function parse(input: number): number[];
function parse(input: string | number): string[] | number[] {
  if (typeof input === "string") {
    return input.split(",");
  }
  return Array.from(String(input), Number);
}

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

thisの型注釈

interface Component {
  name: string;
  render(this: Component): string;
}

const c: Component = {
  name: "Card",
  render() {
    return `<div>${this.name}</div>`; // this は Component
  },
};

戻り値の型推論 vs 明示

// 推論に任せる(短いユーティリティ)
const double = (n: number) => n * 2; // 戻り値推論: number

// 明示する(API境界・ライブラリ・公開関数)
export function fetchUser(id: number): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// 明示するメリット:
// - 実装ミスで意図しない戻り値型になっても即エラー
// - エディタのホバーで型が読みやすい
// - 公開APIの破壊的変更検出

any / unknown / never / void:似て非なる4型

any:型チェックを完全に切る最終手段

// Before(anyの濫用)
function process(data: any) {
  return data.user.name.toUpperCase();
  // ↑ data.user が undefined でもコンパイル通る → 実行時クラッシュ
}

// After(unknown + 型ガード)
function processSafe(data: unknown): string {
  if (
    typeof data === "object" &&
    data !== null &&
    "user" in data &&
    typeof (data as any).user?.name === "string"
  ) {
    return ((data as { user: { name: string } }).user.name).toUpperCase();
  }
  throw new Error("Invalid data");
}

unknown:any の安全な代替

// JSON.parse の戻り値は unknown として扱うのがベスト
function safeParse(json: string): unknown {
  return JSON.parse(json);
}

const data = safeParse('{"name":"Alice"}');
// data.name; // Error: 'data' is of type 'unknown'

// 型ガードで絞り込み
function isUser(x: unknown): x is { name: string } {
  return typeof x === "object" && x !== null && "name" in x &&
         typeof (x as { name: unknown }).name === "string";
}

if (isUser(data)) {
  console.log(data.name); // string
}

never:値が存在しない型

// 関数が決してreturnしない場合の戻り値
function panic(msg: string): never {
  throw new Error(msg);
}

// 無限ループ
function loop(): never {
  while (true) { /* ... */ }
}

// 網羅性チェックでも使う(前出の switch default)
type Status = "ok" | "ng";
function f(s: Status) {
  switch (s) {
    case "ok": return;
    case "ng": return;
    default: const _: never = s;
  }
}

void:戻り値を使わない宣言

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

// void 戻り値の特殊ルール:呼び出し側で何か返してもOK
const handlers: Array<() => void> = [];
handlers.push(() => 123); // OK(戻り値を無視する契約)

// undefined を返すと明示するなら undefined 型
function maybeReturn(): undefined {
  return;
}

4型の比較表

意味代入可能(他型→当該型)取り出し時使いどころ
any型検査を放棄すべて可すべて可移行期の一時退避のみ
unknown未知の型すべて可narrowing必須外部入力の入口
never到達不能不可使えない網羅性・throw専用関数
void戻り値無視undefinedのみ使えないcallback戻り値

as const と satisfies:TS 4.9以降の必修

as const アサーションのおさらい

// without as const
const colors = ["red", "blue"];
//    ^? string[]

// with as const
const colors2 = ["red", "blue"] as const;
//    ^? readonly ["red", "blue"]

// オブジェクトでも
const theme = {
  primary: "#0070f3",
  secondary: "#f00",
} as const;
// theme.primary は "#0070f3" リテラル型

satisfies 演算子(TS 4.9+)の意義

type Config = Record<string, string | string[]>;

// Before(型注釈で潰れる)
const config1: Config = {
  endpoint: "https://api.example.com",
  tags: ["a", "b"],
};
// config1.tags.map(...) で .map は推論できるが、
// config1.endpoint は string で固定。リテラル情報が失われる

// After(satisfies で型チェックしつつリテラルを保つ)
const config2 = {
  endpoint: "https://api.example.com",
  tags: ["a", "b"],
} satisfies Config;

config2.endpoint; // "https://api.example.com" (リテラル)
config2.tags;     // string[] (推論)

satisfies の現場ユースケース

// ルート定義を型チェックしつつ補完を効かせる
type Route = { path: string; auth: boolean };

const routes = {
  home:  { path: "/",       auth: false },
  admin: { path: "/admin",  auth: true  },
} satisfies Record<string, Route>;

// routes.home.path は "/" のリテラル
// routes.admin.auth は true のリテラル

// 設定漏れもエラーで気付ける
// const broken = { home: { path: "/" } } satisfies Record<string, Route>;
// → 'auth' プロパティが欠落しています

import type と verbatimModuleSyntax

型のみインポート

// 値と型が混在
import { fetchUser, User } from "./user";

// 推奨:型は import type で
import { fetchUser } from "./user";
import type { User } from "./user";

// あるいは inline 構文
import { fetchUser, type User } from "./user";

verbatimModuleSyntax の意味

// tsconfig: "verbatimModuleSyntax": true
// → import 文がそのまま出力に残るかどうかが「書いた通り」になる
// → 型だけのimportは必ず import type と明示しないと、
//   未使用バンドルの混入や副作用呼び出し有無が変わるリスクを排除

// Bad
import { User } from "./types"; // 値として残るか型として消えるか曖昧

// Good
import type { User } from "./types"; // 必ず削除される

export type

// 関数(値)と型を区別してエクスポート
export function createUser(name: string): User {
  return { id: Date.now(), name };
}

export type User = {
  id: number;
  name: string;
};

// あるいは再エクスポート
export type { User } from "./types";

実践:any地獄からの脱出リファクタリング

Before:JavaScriptそのまま移植

// Before:全部 any、型の利益ゼロ
function fetchPosts(opts: any): any {
  return fetch(opts.url)
    .then((res: any) => res.json())
    .then((data: any) => {
      return data.posts.filter((p: any) => p.published);
    });
}

const result = fetchPosts({ url: "/api/posts" });
// result.then(posts => posts[0].titl) // typo もすり抜ける

After:型を入れて安全に

type FetchPostsOptions = {
  url: string;
  signal?: AbortSignal;
};

type Post = {
  id: number;
  title: string;
  body: string;
  published: boolean;
};

type PostsResponse = {
  posts: Post[];
  total: number;
};

async function fetchPosts(opts: FetchPostsOptions): Promise<Post[]> {
  const res = await fetch(opts.url, { signal: opts.signal });
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
  const data = (await res.json()) as PostsResponse;
  return data.posts.filter(p => p.published);
}

const posts = await fetchPosts({ url: "/api/posts" });
posts[0]?.title; // string(typoはエディタが補完で防ぐ)

段階移行のコツ

  • まず tsconfig.jsonstrict をON、noImplicitAny 違反だけを潰す
  • 外部APIレスポンスは unknown で受けて型ガードを書く
  • 共通の型は src/types/*.ts に集約し、命名は User / Post など短く
  • any を使うときは // TODO(typing): ... でgrepしやすくしておく
  • ESLintルール @typescript-eslint/no-explicit-anywarn でCIに乗せる

型注釈は「どこに書くか」が9割

書くべき箇所:境界線(API・関数の入口出口)

// 良い:公開関数の引数・戻り値は明示
export function calcTax(price: number, rate: number): number {
  return Math.floor(price * rate);
}

// 良い:外部から受け取る型は明示
type FormValues = {
  name: string;
  email: string;
  age: number;
};

export function submit(values: FormValues): Promise<void> {
  return fetch("/api/submit", { method: "POST", body: JSON.stringify(values) })
    .then(() => undefined);
}

書かなくていい箇所:ローカル変数

// Bad(冗長)
const count: number = 0;
const message: string = "Hello";
const items: string[] = ["a", "b"];

// Good(推論に任せる)
const count = 0;
const message = "Hello";
const items = ["a", "b"];

判断フロー

  1. 公開API(他モジュール・ライブラリ・外部依存)→ 明示
  2. ローカル変数の初期化で型が一意に決まる → 推論
  3. 初期値が空で後から代入(let arr = [])→ 明示(let arr: string[] = [])
  4. JSON.parse・外部fetchの結果 → unknownで受けて型ガード
  5. 戻り値が複雑なジェネリクスを含む → 明示(ホバー時の可読性)

React + TypeScript の最小サンプル

Props の型定義

type ButtonProps = {
  label: string;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
};

export function Button({
  label,
  variant = "primary",
  disabled = false,
  onClick,
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

useState の型推論と明示

// 初期値から推論
const [count, setCount] = useState(0);          // number
const [name, setName] = useState("Alice");      // string

// nullが入り得るなら明示
const [user, setUser] = useState<User | null>(null);

// 配列・オブジェクトも
const [items, setItems] = useState<Item[]>([]);

useReducer + discriminated union

type State = { count: number };
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "set"; payload: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
    case "set":       return { count: action.payload };
    default: {
      const _: never = action;
      return state;
    }
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "set", payload: 100 }); // OK
// dispatch({ type: "set" });            // Error: payload required

よくあるエラーメッセージと解消法

Object is possibly ‘undefined’

const items: string[] = ["a", "b"];
const first = items[0]; // noUncheckedIndexedAccess: true なら string | undefined

// オプショナルチェイニング
first?.toUpperCase();

// 早期return
if (first === undefined) return;
first.toUpperCase(); // ここでは string

// non-null assertion(本当に確実なときだけ)
items[0]!.toUpperCase();

Argument of type ‘X’ is not assignable to parameter of type ‘Y’

type Color = "red" | "blue";

function paint(c: Color) { /* ... */ }

const userInput = "red";        // string と推論される
// paint(userInput);            // Error

// 解決1: as const で literal にする
const userInput2 = "red" as const;
paint(userInput2);              // OK

// 解決2: 型を明示
const userInput3: Color = "red";
paint(userInput3);              // OK

Type ‘string’ is not assignable to type ‘never’

// 空配列の型が never[] になっている
const arr = [];           // any[](strictなら never[] に近い挙動)
// arr.push("a");         // 環境によりエラー

// 解決:初期化時に型を与える
const arr2: string[] = [];
arr2.push("a"); // OK

FAQ

Q. JavaScriptしか書いたことがありません。TypeScriptは難しいですか?

A. 文法はほぼ同じです。差は「変数・引数・戻り値に型を書く」だけ。本記事のプリミティブ型→配列→オブジェクト→ユニオン型の順で進めれば、3日で実務レベルの記述ができるようになります。ジェネリクスやmapped typeは慣れてから学べばOKです。

Q. type と interface はどちらを使えばいいですか?

A. ユニオン型・交差型・mapped typeを使うならtype、クラスとペアで使う・ライブラリの拡張点として公開するならinterfaceがおすすめです。チームで統一されているなら、その方針に従うのが最優先です。

Q. any を絶対に使ってはいけませんか?

A. 段階移行中の一時退避としては許容します。ただし、コメントで // TODO(typing) を残し、ESLintでwarnを出してCIで件数を可視化しましょう。新規コードでは原則unknownを使うべきです。

Q. enum は使うべきですか?

A. 新規プロジェクトではas constオブジェクト方式を推奨します。ツリーシェイキング・--isolatedModulesとの相性・初学者の理解しやすさの3点で優位です。既存のenumを無理に書き換える必要はありません。

Q. tsconfigで何を最初にONにすべきですか?

A. strict: truenoUncheckedIndexedAccess: trueの2つです。前者でnoImplicitAnystrictNullChecks等の主要フラグが一気に有効になり、後者で配列アクセスのundefinedを型に反映できます。

Q. import type と import の通常形はどう違いますか?

A. import typeは型情報だけを取り込み、コンパイル後のJSから完全に削除されます。バンドルサイズに影響しません。値も含むモジュールは通常のimportimport { value, type T }のinline構文を使い分けます。

Q. 学習を加速したいです。スクールでまとめて学ぶ価値はありますか?

A. 独学で詰まる典型は「型エラーの読み方」と「any脱却の判断軸」です。レビュー文化のあるスクール(テックアカデミー・侍エンジニア・DMM WEBCAMP・レバテックカレッジ等)で、現役エンジニアのコードレビューを受けると、自分では気づかないany濫用や型注釈の冗長さを早期に直せます。短期間で実務水準に到達したい方には費用対効果が高い選択肢です。

まとめ

本記事ではTypeScriptの型の基礎を、tsconfig・プリミティブ・配列・タプル・オブジェクト・ユニオン・リテラル・narrowing・as constsatisfiesimport typeまで、40本以上のコード例で網羅した。覚えるべきは「境界に型を書き、内部は推論に任せる」というシンプルな原則だstrict: truenoUncheckedIndexedAccess: trueをONにし、外部入力はunknownで受けて型ガードで絞り込む——これだけで、any地獄からは確実に脱出できる。

次のステップは「ジェネリクスとユーティリティ型」「mapped type / conditional type」「Zod等のスキーマバリデーション」だ。型を書くスピードを上げ、レビューでのフィードバックを最短で受け取りたい場合、現役エンジニアによる伴走レビューがあるスクール(テックアカデミー・侍エンジニア・DMM WEBCAMP・レバテックカレッジ)を活用するのも、実務到達までの近道になる。

コメント

タイトルとURLをコピーしました