understand-type-widening について
宣言方法が型推論に与える影響を理解したい TypeScript 開発者向けのスキルです。プリミティブ、オブジェクト、配列、リテラルの型の拡大と、その制御方法を解説します。
主な機能
- 01let と const による型の違いを解説
- 02オブジェクトのプロパティと配列の拡大を説明
- 03as const でリテラル型を保持
- 04注釈と satisfies による推論の制御
- 05リテラル型とタプルのエラーを診断
活用シーン
- 01文字列リテラルが string になる理由の確認
- 02広すぎるオブジェクト型や配列型への対処
- 03設定値をリテラル型のユニオンに適合
- 04タプルが通常の配列になる問題の防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill understand-type-wideningインストール後、Claude Code で/understand-type-wideningを入力して読み込まれたか確認します。
詳細
型の拡大を理解する
概要
TypeScript は値から型を推論する際、型を拡大することがよくあります。
"x" で初期化された変数は、任意の文字列を保持するつもりかもしれませんし、リテラル型の "x" だけを保持するつもりかもしれません。TypeScript はヒューリスティックに基づいて推測します。これを理解すると、予測しやすいコードを書けるようになります。
このスキルを使う場面
- 型が
"specific-value"ではなくstringになる理由がわからない - 同じ値でも
constとletで型が異なる - 配列リテラルの要素型が予想と異なる
- オブジェクトのプロパティ型が予想より広い
- リテラル型を代入できないという型エラーが発生する
鉄則
ALWAYS understand how your declaration style affects inferred types.
覚えておきましょう:
let変数ではリテラル型が基本型に拡大されますconst変数ではリテラル型が保持されます(プリミティブ型の場合)constを使っても、オブジェクトや配列の中身は拡大されます- リテラル型を完全に推論させるには
as constを使います
検出:予想外の型拡大
TypeScript が予想より広い型を推論している場合、型が拡大されています。
// Primitive widening with let
let x = 'x';
// ^? let x: string (not "x")
// No widening with const (for primitives)
const y = 'y';
// ^? const y: "y"
// But object properties still widen
const obj = { x: 1, y: 2 };
// ^? const obj: { x: number; y: number } (not { x: 1, y: 2 })
型の拡大が存在する理由
TypeScript は、次の2つの目標のバランスを取る必要があります。
- 具体性 - 狭い型を使って実際のバグを検出する
- 柔軟性 - 妥当な変更を許容する
// Without widening, this would fail:
let x = 'x'; // If inferred as "x", then...
x = 'y'; // Error! "y" is not assignable to "x"
// With widening:
let x = 'x'; // Inferred as string
x = 'y'; // OK
型の拡大ルール
ルール1:let では拡大され、const では保持される(プリミティブ型)
// let → widened
let a = 'hello';
// ^? let a: string
let b = 42;
// ^? let b: number
let c = true;
// ^? let c: boolean
// const → literal
const d = 'hello';
// ^? const d: "hello"
const e = 42;
// ^? const e: 42
const f = true;
// ^? const f: true
ルール2:オブジェクトのプロパティは常に拡大される
const point = { x: 3, y: 4 };
// ^? const point: { x: number; y: number }
// Not { x: 3, y: 4 } - because you might do:
point.x = 10; // This must be valid
ルール3:配列の要素は拡大される
const arr = [1, 2, 3];
// ^? const arr: number[]
// Not [1, 2, 3] or readonly [1, 2, 3]
ルール4:要素の型が混在する配列はユニオン型になる
const mixed = [1, 'x'];
// ^? const mixed: (string | number)[]
// TypeScript picks the "best common type"
型の拡大を制御する
リテラル型を完全に推論させるには as const を使う
// Regular object - properties widen
const obj1 = { x: 1, y: 2 };
// ^? const obj1: { x: number; y: number }
// With as const - properties are literal and readonly
const obj2 = { x: 1, y: 2 } as const;
// ^? const obj2: { readonly x: 1; readonly y: 2 }
// Arrays too
const arr = [1, 2, 3] as const;
// ^? const arr: readonly [1, 2, 3]
型注釈を使って明示する
// Annotate to get the type you want
const x: 'x' | 'y' = 'x';
// ^? const x: "x" | "y"
// Can be reassigned to 'y', but nothing else
型チェック付きの推論には satisfies を使う
type Point = { x: number; y: number };
// Type annotation: loses literal types
const p1: Point = { x: 1, y: 2 };
// ^? const p1: Point
// satisfies: keeps literals while checking structure
const p2 = { x: 1, y: 2 } satisfies Point;
// ^? const p2: { x: number; y: number }
p2.x; // number (not 1, but still good for inference)
よくある型の拡大に関する問題
問題:リテラル型が必要なのに、string が得られる
type HTTPMethod = 'GET' | 'POST' | 'PUT';
function makeRequest(method: HTTPMethod) { /* ... */ }
let method = 'GET';
makeRequest(method);
// ~~~~~~ Argument of type 'string' is not assignable
解決策:
// Solution 1: Use const
const method = 'GET';
makeRequest(method); // OK
// Solution 2: Type annotation
let method: HTTPMethod = 'GET';
makeRequest(method); // OK
// Solution 3: as const
let method = 'GET' as const;
makeRequest(method); // OK
問題:オブジェクトのプロパティ型が広すぎる
type Config = {
mode: 'development' | 'production';
debug: boolean;
};
function configure(config: Config) { /* ... */ }
const config = { mode: 'development', debug: true };
configure(config);
// ~~~~~~ Type 'string' is not assignable to type '"development" | "production"'
解決策:
// Solution 1: Type annotation
const config: Config = { mode: 'development', debug: true };
// Solution 2: as const on the whole object
const config = { mode: 'development', debug: true } as const;
// Solution 3: as const on just the property
const config = { mode: 'development' as const, debug: true };
問題:タプルが配列になる
function setPoint(point: [number, number]) { /* ... */ }
const coords = [10, 20];
setPoint(coords);
// ~~~~~~ Type 'number[]' is not assignable to type '[number, number]'
解決策:
// Solution 1: Type annotation
const coords: [number, number] = [10, 20];
// Solution 2: as const (makes it readonly)
const coords = [10, 20] as const;
// Note: readonly [10, 20] may not be assignable to [number, number]
// depending on the function's type
圧力に屈しないための手順
1. 「とにかく any を使う」
圧力:「型がおかしいなら、とにかく any にキャストすればいい」
**回答:**型は正しく、望むより広いだけです。
対応:const、型注釈、または as const を使います。
2. 「TypeScript はばかげている」
圧力:「明らかに 'GET' なのに、なぜ string と推論するのか」
**回答:**TypeScript は let 変数の値が変わると想定しています。
**対応:**変更されない値には const を使います。
危険信号 — 作業を止めて再検討する
- リテラル型のエラーをキャストで解決しようとしている
- オブジェクトのプロパティが
"specific"ではなくstringになることに驚いた - タプル型が配列になっている
- ユニオンのリテラル型が予期せず拡大される
よくある正当化(すべて誤り)
| 言い訳 | 実際のところ |
|---|---|
| 「const ならリテラル型になるはず」 | プリミティブ型に限られ、オブジェクトの中身には当てはまりません |
| 「値は明らかに X だ」 | let は値が変わる可能性を意味します |
| 「as any で解決できる」 | 適切な型の絞り込み方法を使いましょう |
クイックリファレンス
| 宣言 | 値 | 推論される型 |
|---|---|---|
let x = "hello" | 文字列リテラル | string |
const x = "hello" | 文字列リテラル | "hello" |
const x = { a: 1 } | オブジェクト | { a: number } |
const x = { a: 1 } as const | オブジェクト | { readonly a: 1 } |
const x = [1, 2] | 配列 | number[] |
const x = [1, 2] as const | 配列 | readonly [1, 2] |
要点
TypeScript は妥当な変更を許容するために型を拡大します。
プリミティブ型には const を使い、型を明示するには型注釈を使い、リテラル型を完全に推論させる必要がある場合は as const を使いましょう。型の拡大を理解すると、予想外の型エラーを防ぎ、より正確な型を書けるようになります。
参考資料
Dan Vanderkam 著『Effective TypeScript』項目20「変数の型がどのように決まるかを理解する」に基づいています。