← 解説「データモデルの型設計 — interfaceは画面の設計図」に戻る
サンプルコードで身につける: データモデルの型設計 — interfaceは画面の設計図
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1APIレスポンスに interface で型を付ける
APIが返すJSONの形をそのまま interface に写し、HttpClient の型引数に渡します。これだけでフィールド名の打ち間違いや型違いの計算を、実行する前にコンパイラが捕まえてくれます。
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// APIが返すJSONの形を interface に写す(= データの設計図)
export interface Product {
id: number;
name: string;
price: number;
stock: number;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
fetchAll() {
// 型引数を渡すと、受け取った後の扱いがすべてチェックされる
return this.http.get<Product[]>('/api/products');
}
}
// p.nama → コンパイルエラー: Property 'nama' does not exist
// p.price * 1.1 → OK(number だと分かっているから計算できる)
2Before/After — any で逃げると壊れるのが実行時になる
any は型チェックを丸ごと無効にするスイッチです。エラーは消えますが、打ち間違いが画面の undefined 表示として本番で発覚します。壊れる時点が「今」から「実行時」へ先送りされるだけです。
// ── Before: any で逃げた場合 ──
// this.http.get<any>('/api/products').subscribe((list) => {
// this.total = list[0].prise * list.length;
// // ^^^^^ price の打ち間違いだが、エラーは出ない
// });
// → コンパイルは通る。実行すると undefined * 3 = NaN。
// 画面に「合計: NaN円」と出て、本番で初めて気付く
// ── After: 型を付けた場合 ──
// this.http.get<Product[]>('/api/products').subscribe((list) => {
// this.total = list[0].prise * list.length;
// // ^^^^^ コンパイルエラー:
// // Property 'prise' does not exist on type 'Product'.
// // Did you mean 'price'?
// });
// → 書いたその場でエディタが赤線を引く。実行前に直せる
3画面用の型を分けるタイミング
APIの型に画面用の計算値を混ぜ始めたら分け時です。APIの型はサーバーの形をそのまま、画面用の型は表示に必要な形を持ち、変換関数で橋渡しすると変更の影響が閉じます。
// ── Before: APIの型に画面の都合を混ぜてしまった ──
// export interface Product {
// id: number;
// name: string;
// price: number;
// priceWithTax?: number; // 画面で計算した値。APIは返さないので optional…
// isLowStock?: boolean; // これもAPIには存在しない
// }
// → どのフィールドが本物のAPI由来か分からなくなっていく
// ── After: APIの型と画面用の型を分け、変換関数でつなぐ ──
export interface ProductResponse {
id: number;
name: string;
price: number; // APIが返すままの形(サーバーの都合で変わる)
stock: number;
}
export interface ProductView {
id: number;
name: string;
priceLabel: string; // 表示用: '1,320円(税込)'
isLowStock: boolean; // 表示用: 在庫わずか表示の判定
}
export function toProductView(res: ProductResponse): ProductView {
return {
id: res.id,
name: res.name,
priceLabel: Math.round(res.price * 1.1).toLocaleString() + '円(税込)',
isLowStock: res.stock < 5,
};
}
4optional と union で「無いかもしれない」を設計図に書く
APIが返さないことのあるフィールドは ? を付け、決まった文字列しか来ない値は union 型にします。あり得る形を型に書いておくほど、扱い漏れをコンパイラが見つけてくれます。
// 「無いかもしれない」「これしか来ない」も設計図に書く
export interface Order {
id: number;
// 決まった値しか来ないなら union 型にする
// → status === 'shiped' のような打ち間違いが即エラーになる
status: 'pending' | 'paid' | 'shipped' | 'cancelled';
// 発送前は無いフィールドには ? を付ける
shippedAt?: string;
}
export function shippingLabel(order: Order): string {
// shippedAt は string | undefined なので、
// チェックせずに使おうとするとコンパイラが指摘してくれる
if (order.shippedAt) {
return order.shippedAt + ' に発送済み';
}
return '未発送';
}
5型ファイルの置き場所 — model.ts に集める
型はサービスのファイルに埋め込まず、機能フォルダの model.ts にまとめます。コンポーネントとサービスの両方が同じ設計図を参照でき、「この機能のデータの形」を1ファイルで見渡せます。
// ── Before: 型が product.service.ts の中に埋まっている ──
// コンポーネントが型だけ欲しいのにサービスを import する羽目になり、
// データの形を確認するにもサービスのコードをかき分けることになる
// ── After: features/products/product.model.ts に集める ──
//
// features/products/
// ├── product.model.ts # ★ この機能のデータの形はここを見れば分かる
// ├── product.service.ts # import { Product } from './product.model';
// └── product-card.ts # import { Product } from './product.model';
// product.model.ts の中身は型と純粋な変換関数だけにする
export interface Product {
id: number;
name: string;
price: number;
stock: number;
}
export function isInStock(product: Product): boolean {
return product.stock > 0;
}