typescript-advanced について
高度なTypeScriptパターンとtype-festのユーティリティを活用し、安全で保守しやすいコードを書くためのスキルです。TypeScript開発者やコーディングエージェントを対象に、型機能をいつ、どう使うかを重視し、API全般のリファレンスとは異なる構成です。
主な機能
- 01unknownとneverによる型安全性
- 02判別可能なユニオンと網羅性チェック
- 03ブランド型による名目的な安全性
- 04satisfiesとNoInferによる推論制御
- 05type-festユーティリティの選択
- 06よくある型の落とし穴への指針
活用シーン
- 01IDや検証済み値の安全な型付け
- 02型付き設定やレジストリーの構築
- 03ユニオンの全バリアントを網羅的に処理
- 04複雑な型変換に使うユーティリティの選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trancong12102/ agentskills --skill typescript-advancedインストール後、Claude Code で/typescript-advancedを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/conditional-types.mdmapped-types.mdmodule-augmentation.mdtemplate-literals.mdtype-fest.md詳細
TypeScript 上級:パターン、落とし穴、type-fest
このスキルでは、高度な TypeScript を記述する際のルール、規約、アーキテクチャ上の決定を定めています。型レベルでよくあるバグを防ぎ、安全で保守しやすいコードにつながるパターンを徹底するため、意図的に特定の方針を採用しています。
TypeScript の機能に関する詳細な API ドキュメントは、適切なほかのツール(ドキュメント検索、ウェブ検索など)を利用してください。このスキルでは、高度な型機能をいつ、なぜ、どのように正しく使うかに焦点を当てています。
型安全性の考え方
any、unknown、never の違い — 覚えておくべき唯一のルール
| 型 | 代入元 | 代入先 | 操作 | 用途 |
|---|---|---|---|---|
any | 何でも | 何でも | すべて(安全でない) | 新しいコードでは使わない |
unknown | 何でも | unknown / any のみ | なし(絞り込み前) | 外部入力、JSON、ユーザーデータ |
never | 何もない | 何でも | なし | 網羅性チェック、到達不能なコード |
ルール:新しいコードでは any を決して使わないでください。 外部との境界では unknown を使い、操作する前に型を絞り込んでください。網羅性の確保と不可能な状態の表現には never を使います。
enum よりユニオン型を優先する
// Avoid — numeric enums are structurally assignable to number (footgun)
enum Direction {
Up,
Down,
Left,
Right,
}
function go(d: Direction) {}
go(42); // no error — TypeScript allows any number!
// Prefer — exhaustive, tree-shakeable, no runtime artifact
type Direction = "up" | "down" | "left" | "right";
文字列 enum は数値 enum より安全ですが、それでも実行時のオーバーヘッドやインポート時の扱いにくさがあります。逆引きが必要な場合を除き、文字列リテラルのユニオン型をデフォルトとして選びます。
interface と type — 選択表
| シナリオ | 使用する型 | 理由 |
|---|---|---|
| オブジェクトの形状、クラスの契約 | interface | 宣言のマージに対応し、エラーメッセージも分かりやすい |
| ユニオン、交差、マップ型/条件型 | type | これらに対応しているのは type のみ |
| サードパーティの拡張が必要 | interface | 宣言のマージに対応しているのは interface のみ |
| パブリック API の型(ライブラリ) | interface | 利用者が拡張でき、ツールチップでの表示も分かりやすい |
| 内部の計算型 | type | より表現力が高く、意図しないマージも起きない |
判別可能なユニオン型と網羅性チェック
never による網羅性確保のパターン
判別可能なユニオン型に対するすべての switch / if-else チェーンでは、各バリアントを処理する必要があります。
新しいバリアントが追加されたときにコンパイル時エラーを発生させるには、never への代入を使います。
type Result<T> =
| { status: "ok"; data: T }
| { status: "error"; error: Error }
| { status: "loading" };
function handle<T>(result: Result<T>): string {
switch (result.status) {
case "ok":
return JSON.stringify(result.data);
case "error":
return result.error.message;
case "loading":
return "Loading...";
default:
const _exhaustive: never = result;
return _exhaustive; // compile error if a variant is unhandled
}
}
判別可能なユニオン型のルール
- 判別プロパティはリテラル型にする必要があります —
string、number、booleanのリテラルを使います。stringのような広い型では絞り込まれません。 - すべてのメンバーで判別プロパティ名を統一してください(
kind、type、statusなど)。 - 判別プロパティをオプショナルにするのは避けてください —
status?: "ok" | "error"は型の絞り込みを壊します。
ブランド型 — 構造的型付けシステムにおける名目的な安全性
TypeScript は構造的型付けです。デフォルトでは、UserId(string)と OrderId(string)は相互に代入できます。ブランド型を使うと、実行時のオーバーヘッドなしに型レベルでこれを防げます。
推奨パターン:unique symbol ブランド
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// Constructor = the single trust boundary, validate here
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;
function getUser(id: UserId) {
/* ... */
}
getUser(toUserId("abc")); // ok
getUser(toOrderId("abc")); // ERROR — OrderId not assignable to UserId
getUser("abc"); // ERROR — string not assignable to UserId
ブランド型を使う場面
- ID —
UserId、OrderId、ProductIdを使うと、相互に誤って代入するのを防げます - 単位 —
Meters、Feet、USD、EURを使うと、計算ミスを防げます - 検証済みの文字列 —
Email、URL、Slugで、検証済みであることを表現できます - 不透明なトークン —
JWTToken、APIKeyを使うと、誤ってログに記録したり表示したりするのを防げます
type-fest を使う代替方法
複数のタグの合成やメタデータには、type-fest の Tagged<T, Tag> を使います。
import type { Tagged, GetTagMetadata } from "type-fest";
type UserId = Tagged<string, "UserId">;
type AdminId = Tagged<UserId, "Admin">; // composable — both tags preserved
最新の型推論ツール
satisfies — 型を広げずに検証する(TS 4.9 以降)
type Theme = Record<"primary" | "secondary", string | string[]>;
// Type annotation: loses specific types
const t1: Theme = { primary: "#000", secondary: ["#111", "#222"] };
t1.secondary.map((s) => s); // ERROR — string | string[] has no .map
// satisfies: validates structure, keeps specific inference
const t2 = { primary: "#000", secondary: ["#111", "#222"] } satisfies Theme;
t2.secondary.map((s) => s); // ok — inferred as string[]
satisfies を使う場面: キーのタイプミスを検出するために設定を検証しつつ、特定の値に対するオートコンプリートも必要な場合です。
const 型パラメーター — ジェネリックなリテラル型の推論(TS 5.0+)
// Without const: T = string[]
function routes<T extends string[]>(r: T): T {
return r;
}
// With const: T = readonly ["users", "posts"]
function routes<const T extends string[]>(r: T): T {
return r;
}
const r = routes(["users", "posts"]); // readonly ["users", "posts"]
const 型パラメーターを使う場面: レジストリー、設定ファクトリー、または呼び出し側でリテラル型を保持することが重要なジェネリックを構築する場合です。
NoInfer<T> — 推論元を制御する(TS 5.4+)
パラメーターが型推論に寄与するのを防ぎます。T を読み取りますが、T がどのような型になるかには影響しません。
function createFSM<const TState extends string>(config: {
states: TState[];
initial: NoInfer<TState>; // must be from states, can't introduce new values
}) {
/* ... */
}
createFSM({ states: ["idle", "running"], initial: "idle" }); // ok
createFSM({ states: ["idle", "running"], initial: "stopped" }); // ERROR
NoInfer を使う場面: 関数に同じ型パラメーターを共有する複数のパラメーターがあり、そのうちの一つを新しい候補として推論に加えるのではなく、ほかのパラメーターから推論される型に制約したい場合です。
type-fest:車輪の再発明を避ける
type-fest は、実行時のコストがゼロ(型のみ)のユーティリティ型を200種類以上提供します。カスタムのユーティリティを書く前に、必ず type-fest を確認してください。
import type { Simplify, Merge, SetRequired, LiteralUnion } from "type-fest";
選択表:組み込み機能と type-fest
| 必要なもの | 組み込み機能 | type-fest |
|---|---|---|
| キーをオプション/必須にする | Partial, Required | SetOptional, SetRequired(キーごと) |
| ディープな部分型/読み取り専用型 | — | PartialDeep, ReadonlyDeep |
| 2つの型をマージする(交差ではなく上書き) | — | Merge, MergeDeep |
| 可読性のために交差型を平坦化する | — | Simplify |
オートコンプリート対応の string 型ユニオン | — | LiteralUnion |
| 最低1つ/厳密に1つの制約 | — | RequireAtLeastOne, RequireExactlyOne |
| 名目的/ブランド付き型 | — | Tagged, UnwrapTagged |
| JSON のラウンドトリップ型 | — | Jsonify |
| 厳密な omit(キーの存在が必須) | Omit(緩い) | Except(厳密) |
| ドット区切りの深いパスアクセス | — | Paths, Get |
| 厳密なオブジェクト(余分なプロパティを拒否) | — | Exact |
| 値の型による pick/omit | — | ConditionalPick, ConditionalExcept |
| Package.json/tsconfig の型 | — | PackageJson, TsConfigJson |
| キーの大文字小文字変換 | — | CamelCasedProperties など |
特に使用頻度の高いユーティリティ
Simplify<T>—A & B & Cを読みやすい{ ...すべてのキー }に平坦化します。ホバー時のツールチップが読みにくくなる交差型に使用してください。Merge<A, B>— キーが競合するとA & Bはneverになりますが、Mergeはきれいに上書きします。型でキー名が重複する場合は、&の代わりに使用してください。LiteralUnion<Literal, Base>—'a' | 'b' | stringはオートコンプリートを無効にしますが、LiteralUnionはそれを維持します。拡張可能な文字列 API に不可欠です。SetRequired<T, K>/SetOptional<T, K>— 重複するインターフェースを維持せずに、特定のキーを切り替えます。Jsonify<T>—JSON.parse(JSON.stringify(x))の動作をモデル化します。Date→string、undefined→ 削除、インターフェースのオープンインデックスに関する問題を検出します。
よくある落とし穴
-
anyが気付かないうちに漏れ出します — ひとつのanyが、代入、ジェネリクス、戻り値の型へと伝播します。ユーティリティ型にひとつでもanyがあると、その後のすべての型が型安全ではなくなります。代わりにunknownと型の絞り込みを使用してください。 -
余分なプロパティのチェックはリテラルにしか適用されません — 変数を介して代入すると、余分なプロパティのチェックは完全に回避されます。実行時の安全性を確保するために、このチェックに頼らないでください。
interface Point { x: number; y: number; } const obj = { x: 1, y: 2, z: 3 }; const p: Point = obj; // no error — z slips through -
neverに対する分配条件型 —Tがneverの場合、T extends X ? A : BはBではなくneverを返します。タプルでラップしてください:[T] extends [X]。 -
Omitはキーの存在をチェックしません —Omit<T, "typo">は何事もなく成功します。厳密にキーをチェックするには、type-fest のExceptを使用してください。 -
letによる型の拡大 —let x = "hello"の型は"hello"ではなくstringです。リテラル型を保持するには、const、as const、またはsatisfiesを使用してください。 -
キーが競合する場合の
&による交差 —{ a: string } & { a: number }では、aの型がneverになります。代わりに type-fest のMergeを使用してください。 -
列挙型の数値代入可能性 —
enum Foo { A, B }では、const x: Foo = 999が許可されます。代わりに文字列リテラルのユニオンを使用してください。 -
interfaceの意図しないマージ — 同じスコープにinterface User {}の宣言がふたつあると、気付かないうちにマージされます。拡張されるべきではない内部型にはtypeを使用してください。 -
isolatedModulesでのconst enum— esbuild、SWC、Babel はいずれもisolatedModulesを使用します。.d.tsやライブラリコード内のconst enumは、これらのビルドを壊します。 -
配列パラメーターでの
readonlyの付け忘れ —function f(arr: string[])では変更が可能です。変更する意図のないパラメーターにはreadonly string[]を使用してください。 -
関数パラメーターの構造的部分型付け — メソッド構文の
push(x: T)は双変です(型安全ではありません)。正しい分散を実現するには、strictFunctionTypesのもとで関数プロパティ構文push: (x: T) => voidを使用してください。 -
type-fest ユーティリティを再発明する —
DeepPartial、DeepReadonly、Merge、ブランド型、またはキー操作型を書く前に、type-fest を確認してください。このライブラリは、手作業で実装したものでは見落とされるエッジケース(循環参照、読み取り専用配列、マップ/セット)にも対応しています。
参考ファイル
特定のパターンを扱う際は、関連する参考ファイルを読んでください:
| ファイル | 読むタイミング |
|---|---|
references/conditional-types.md | infer、分配条件型、extends による制約 |
references/mapped-types.md | キーの再マッピング、フィルタリング、テンプレートリテラルによるキー操作 |
references/template-literals.md | 型レベルでの文字列操作、パターンマッチング、解析 |
references/module-augmentation.md | 宣言のマージ、サードパーティ型の拡張、グローバルスコープ |
references/type-fest.md | 使用例付きのカテゴリ別 type-fest ユーティリティ全一覧 |