Mapped typeも通常と同じ型チェックを受ける理由
{ [K in keyof T]?: T[K] }のようなmapped typeで生成した型は、見た目は特殊でも、実体は通常のinterfaceと同じようにプロパティごとの型チェックが行われます。生成された型だからといって検査が緩くなることはありません。
エラーメッセージの読み方
main.ts(6,47): error TS2322: Type 'string' is not assignable to type 'number'.
main.ts- ファイル名
6- 行番号
47- 列番号 — この位置でtscが検査に失敗しました
TS2322- エラーコード — 検索するとTypeScriptの解説が見つかります
Type 'string' is not assignable to type 'number'.- 内容 — 期待していたもの、または受け付けられなかったもの
このエラーが出る典型パターン
パターン1
1 interface Config { 2 host: string; 3 port: number; 4 } 5 type PartialConfig = { [K in keyof Config]?: Config[K] }; 6 const c: PartialConfig = { host: "localhost", port: "8080" }; ^ 7 console.log(c);
PartialConfigはConfigの各プロパティをoptionalにしただけで、型自体はConfig[K]のまま維持されます。portには文字列を代入できません。
直し方: "8080" を 8080 にします。
パターン2
1 interface Flags { 2 active: boolean; 3 count: number; 4 } 5 type ReadonlyFlags = { readonly [K in keyof Flags]: Flags[K] }; 6 const f: ReadonlyFlags = { active: true, count: "3" }; ^ 7 console.log(f);
ReadonlyFlagsはreadonlyを付けただけで、各プロパティの型はFlagsのままです。countには数値しか入りません。
直し方: "3" を 3 にします。
パターン3
1 interface Settings { 2 volume: number; 3 muted: boolean; 4 } 5 type PartialSettings = { [K in keyof Settings]?: Settings[K] }; 6 const s: PartialSettings = { volume: 10, muted: "yes" }; ^ 7 console.log(s);
PartialSettingsのmutedはboolean型のままです。文字列の"yes"は代入できません。
直し方: "yes" を true にします。
パターン4
1 interface Prefs { 2 theme: string; 3 volume: number; 4 } 5 type PartialPrefs = { [K in keyof Prefs]?: Prefs[K] }; 6 const p: PartialPrefs = { theme: "dark", volume: "5" }; ^ 7 console.log(p);
PartialPrefsはPrefsの各プロパティをoptionalにしただけで、型自体はPrefs[K]のまま維持されます。
直し方: "5" を 5 にします。
パターン5
1 interface Meta { 2 title: string; 3 draft: boolean; 4 } 5 type ReadonlyMeta = { readonly [K in keyof Meta]: Meta[K] }; 6 const m: ReadonlyMeta = { title: "x", draft: "yes" }; ^ 7 console.log(m);
ReadonlyMetaはreadonlyを付けただけで、各プロパティの型はMetaのままです。
直し方: "yes" を true にします。
よくある誤解
「mapped typeは動的に生成される特別な型だから、通常の型チェックの対象外では」という予想は誤りです。tscはmapped typeを展開した結果としての具体的なプロパティ型を、通常のオブジェクト型と同じ精度でチェックします。
実務での勘所
tscはmapped typeを見つけると、それを特別な型として保持し続けるのではなく、対象の型が実際に持つキーを1つずつたどって具体的なプロパティの並びへと展開(解決)します。Partial<User>やReadonly<User>のような組み込みのユーティリティ型も、この展開の仕組みを使って定義されているだけです。展開が終わった時点で、mapped typeは手書きのinterfaceとエディタ上の型情報としても完全に同じものとして扱われます。