要約
作られた場所がはっきりしない値の Optional Property にアクセスするときは、 undefined でないことのチェックではなく、積極的な型チェックが必要。
実例
type A = {
b: boolean;
n: number;
s: string;
}
type B = {
b: boolean;
n: number;
}
type C = {
b: boolean;
n: number;
s?: number;
}
const a: A = { b: true, n: 10, s: 'text' }; // OK
const b: B = a; // A は {b: boolean, n: number} を満たすのでOK
const c: C = b; // おや……? TypeScript の様子が……
if (c.s !== undefined) { // number|undefined -> number に絞り込み
console.log(c.s.toFixed(10)); // 実体は string なので実行時エラー
}
結論
Optional Property にアクセスするときは、消極的な non-undefined チェックによる絞り込みではなく、積極的な型チェックをしましょう。
const a: A = { b: true, n: 10, s: 'text' }; // OK
const b: B = a; // A は {b: boolean, n: number} を満たすのでOK
const c: C = b; // おや……? TypeScript の様子が……
if (typeof c.s === 'number') { // 積極的チェックで安心
console.log(c.s.toFixed(10)); // 通らない
}
補足
オーバーロードも単なるアサーションなので気を付けましょう。
function foo(s: string): string;
function foo(n: number): number;
function foo(v: string | number): string | number {
// オーバーロードの定義とは逆の型の値を返せちゃう。
return typeof v === 'string' ? Number(v) : String(v);
}
console.log(foo(5).toFixed(10)); // 実行時エラー
実際に起こりそうなパターン
type A = {
n: number;
s: string;
};
const trans = (b: { n: number; s?: number }): void => {
b.s = 5;
};
const trans2 = (a: { n: number }): void => {
trans(a);
};
const a: A = { n: 5, s: 'text' };
trans2(a); // A の部分型を期待する関数にaを投げたら……
console.log(a.s.toUpperCase()); // 実行時エラー, 型が壊れていた