TypeScript を書いているのに、気づけば any と型アサーション(as)だらけ——それでは「型のあるJavaScript」でしかありません。本記事は、コピペしてすぐ試せる実践テクニックを10個だけ厳選しました。どれも現場で「バグを型で潰す」ために効くものばかりです。
結論から言うと、押さえるべき軸は3つ。
①推論を殺さない(satisfies / as const)、②状態を型で表す(判別可能ユニオン / Branded Types)、③新しい構文で書く量を減らす(const型パラメータ / NoInfer / using)。この順で見ていきます。
すべて TypeScript 5.x 系で動作します(各機能の導入バージョンは見出しに併記)。
1. satisfies で「型チェック」と「推論」を両立する(4.9〜)
型注釈(: 型)を付けると型チェックは効きますが、各プロパティの型が広くなって推論を潰してしまいます。satisfies は「この型を満たすかチェックはするが、推論はそのまま残す」演算子です。
type RGB = readonly [number, number, number];
type Color = RGB | string;
// ✅ Record<string, Color> を満たすか検査しつつ、各値は最も狭い型のまま
const palette = {
primary: "#5b8cff",
danger: [255, 0, 0],
} satisfies Record<string, Color>;
palette.primary.toUpperCase(); // OK: primary は string と推論される
palette.danger[0].toFixed(); // OK: danger は number のタプルのまま
// ❌ 型注釈だと primary が Color に広がり .toUpperCase() が使えない
// const bad: Record<string, Color> = { ... };「設定オブジェクトの形は縛りたいが、値の具体的な型は活かしたい」——そんな場面の定番です。まず satisfies を体に入れましょう。
2. as const で"魔法の文字列"をリテラル型に固める
as const を付けるとオブジェクトや配列が読み取り専用+リテラル型になります。定数から型を生成すれば、値と型の二重管理から解放されます。
const STATUS = {
Todo: "todo",
Doing: "doing",
Done: "done",
} as const;
// 値から型を作る(enum 不要)
type Status = (typeof STATUS)[keyof typeof STATUS];
// => "todo" | "doing" | "done"
function move(next: Status) {}
move(STATUS.Done); // OK
move("gone"); // ❌ コンパイルエラー(タイポを型で検出)enum の代わりに as const オブジェクトを使うのは、2026年の実務では定番の選択です。ランタイムのJSがそのままオブジェクトになり、tree-shaking もしやすくなります。
3. 判別可能ユニオン + never で「網羅漏れ」を型でブロック
共通のタグ(判別子)を持つユニオンにすると、switch で安全に分岐できます。さらに default で never に代入することで、ケースの追加忘れをコンパイルエラーにできます。
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: {
// ここに来る値は never のはず。
// Shape に "triangle" を足して case を書き忘れると↓でエラーになる
const _exhaustive: never = shape;
return _exhaustive;
}
}
}この「exhaustiveness check(網羅性チェック)」は、状態が増えがちなReducerやAPIレスポンスの分岐で絶大な効果を発揮します。
4. Branded Types で「IDの取り違え」を防ぐ(公称型)
TypeScript は構造的型付けなので、string の UserId と PostId は区別されません。ブランド(幽霊プロパティ)を足すと、疑似的に公称型(nominal typing)を再現できます。
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
const asUserId = (id: string) => id as UserId;
function fetchUser(id: UserId) {/* ... */}
fetchUser(asUserId("u_1")); // OK
fetchUser("u_1"); // ❌ ただの string は渡せない
const p: PostId = asUserId("u_1"); // ❌ UserId を PostId に代入できない実行時のコストはゼロ(ただの string)なのに、引数の取り違えという頻出バグをコンパイル時に根絶できます。金額・ミリ秒・メールアドレスなど「形は同じでも意味が違う値」に効きます。
5. Record / Pick / Omit を使いこなす(ユーティリティ型)
型を毎回手書きするのは非効率です。組み込みのユーティリティ型で、既存の型から派生型を機械的に作りましょう。
interface User {
id: string;
name: string;
email: string;
password: string;
}
// API で返す公開ユーザー(機密フィールドを除外)
type PublicUser = Omit<User, "password">;
// 入力フォームで使う一部だけ
type UserForm = Pick<User, "name" | "email">;
// id をキーにしたマップ
type UsersById = Record<string, User>;
// すべて任意 & 読み取り専用にした patch 用型
type UserPatch = Readonly<Partial<Pick<User, "name" | "email">>>;Partial / Required / Readonly / Pick / Omit / Record / Exclude / Extract の8つは丸暗記して損なし。元の型を1つ直せば派生型が全部追従するのが最大の利点です。
6. テンプレートリテラル型で「文字列の形」を型にする
文字列そのもののフォーマットを型で表現できます。ルーティングやCSS変数名、イベント名などで威力を発揮します。
type Lang = "ja" | "en";
type Page = "home" | "about";
type Route = `/${Lang}/${Page}`;
// => "/ja/home" | "/ja/about" | "/en/home" | "/en/about"
const go = (path: Route) => {};
go("/ja/home"); // OK
go("/fr/home"); // ❌ 定義外の組み合わせはエラー
// イベント名を on〜 に変換する例
type Handler<E extends string> = `on${Capitalize<E>}`;
type ClickHandler = Handler<"click">; // "onClick"7. const 型パラメータで引数を自動リテラル化(5.0〜)
ジェネリック関数に <const T> と書くと、呼び出し側が as const を付けなくてもリテラル型で推論してくれます。設定ビルダーやタプル操作の定番です。
// const なし
declare function make1<T>(config: T): T;
const a = make1({ mode: "dark" });
// a.mode は string に広がる
// const 型パラメータあり(TS 5.0〜)
declare function make2<const T>(config: T): T;
const b = make2({ mode: "dark" });
// b.mode は "dark" のまま! 呼び出し側の as const が不要8. NoInfer<T> で推論の暴走を止める(5.4〜)
複数の引数から型パラメータを推論させたくない場面があります。NoInfer<T> を付けた引数は推論の材料から除外され、意図した候補だけで型が決まります。
// colors だけから C を推論し、defaultColor はそれに従わせたい
function createStreetLight<C extends string>(
colors: C[],
defaultColor: NoInfer<C>,
) {
/* ... */
}
createStreetLight(["red", "yellow", "green"], "red"); // OK
createStreetLight(["red", "yellow", "green"], "blue"); // ❌ "blue" は候補外
// NoInfer が無いと C は "red"|"yellow"|"green"|"blue" に広がり、
// タイポの "blue" を検出できなかった9. using で「後始末」を確実にする(5.2〜)
using 宣言を使うと、変数がスコープを抜けたときに自動で [Symbol.dispose]() を呼びます。try/finally の書き忘れによるリソースリークを防げます(トランスパイル対象と Symbol.dispose のポリフィルが必要)。
function openConnection() {
console.log("open");
return {
query: (sql: string) => {/* ... */},
[Symbol.dispose]() {
console.log("close"); // スコープを抜けると自動で呼ばれる
},
};
}
function run() {
using conn = openConnection();
conn.query("SELECT 1");
} // ← ここで自動的に close される(例外が出ても確実に)
// 非同期の後始末には await using + [Symbol.asyncDispose]() を使う10. 関数の型を再利用する(ReturnType / Awaited / Parameters)
既存の関数から戻り値・引数の型を抜き出して再利用できます。型を二重定義せず、実装が正となる状態を保てます。
async function fetchUser(id: string) {
return { id, name: "Alice", age: 20 };
}
// Promise を剥がして戻り値の中身だけ取り出す
type User = Awaited<ReturnType<typeof fetchUser>>;
// => { id: string; name: string; age: number }
// 引数の型もそのまま使える
type FetchArgs = Parameters<typeof fetchUser>; // [id: string]
const cache = new Map<string, User>();サーバのレスポンス型をフロントで再定義しがちですが、ReturnType と Awaited を組み合わせれば「関数を1つ直せば型も追従」という理想状態に近づけます。
まとめ:型は"後付けの飾り"ではなく"設計そのもの"
今回の10個を一言でまとめると——推論を殺さず(1・2・7)、状態を型で表し(3・4・6)、派生と再利用で二重管理をなくし(5・10)、新構文で書く量を減らす(8・9)。
- まず satisfies と as const で「推論を活かす」感覚を掴む
- 判別可能ユニオン + never で状態の網羅漏れを潰す
- Branded Types で意味の違う値の取り違えを防ぐ
- ユーティリティ型と ReturnType/Awaited で型の二重管理をやめる
どれも any や as を減らし、「バグを実行前に型で捕まえる」ための道具です。まずは1つ、次のPRで satisfies から使ってみてください。リント環境を整える話は、Rust製オールインワンの Biome へ乗り換える記事も参考にどうぞ。
ESLint+Prettierを卒業|Biomeで爆速リント【2026】ESLint と Prettier、設定ファイルもプラグインも競合ももう限界——2026年のトレンドは Rust 製オールインワン「Biome」です。3コマンドで移行でき、フォーマットは Prettier の約35倍高速。コピペで使える biome.json と VS Code / CI 設定まで、10分で終わる乗り換え手順を丸ごと紹介します。frontendlab.magicgifted.com




