項目48 健全性の罠を回避する
すべてのシンボルの静的な型が、実行時の値と互換性があることが保証されている場合、健全である
逆に、シンボルの実行時の値が静的な型から剥離することを「不健全性」という
xは実行時の値はundefinedだが、型はnumberとして推論されているため不健全
code:ts
const xs = 0, 1, 2;
// ^? const xs: number[]
const x = xs3;
// ^? const x: number
健全でない型は、実行時エラーを引き起こしやすいため、健全な型システムは一般的にプログラミング言語にとって望ましい性質
型システムには表現力・健全性・利便性の3つのトレードオフが伴う
ジェネリック型は便利だが、不健全性を生みやすい
strictNullChecksを有効にすると、健全性の向上を引き換えに、多少の不便さを受け入れることになる
不健全性との向き合い方
any
anyは型エラーは起こらないが、実行時エラーが発生する不健全性の原因になり得る
code:ts
function logNumber(x: number) {
console.log(x.toFixed(1));
}
const num: any = 'forty two';
logNumber(num); // 型エラーが発生しない
解決策
any型の使用を制限する or 全く使わない
代わりにunknown型を使う
型アサーション
anyの少し問題のない親戚が型アサーション
code:ts
function logNumber(x: number) {
console.log(x.toFixed(1));
}
const hour = (new Date()).getHours() || null;
// ^? const hour: number | null
logNumber(hour);
// ~~~~ ... Type 'null' is not assignable to type 'number'.
logNumber(hour as number); // 型チェックはパスするが、実行時に失敗する可能性がある
改善策
型の絞り込みを使う
code:ts
if (hour !== null) {
logNumber(hour); // OK
}
TypeScriptの型と実行時の検証ロジックを動悸させるために、体系的なアプローチを取るのが良い
詳しくは項目74 実行時に型を再構築する方法を知る
オブジェクトや配列へのアクセス
strictモードであっても、TypeScriptは配列にアクセスする際に、境界チェックを行わない
これは不健全性や実行時エラーに直結する
上記のチェックを有効化したい場合は、noUncheckedIndexdAccessをオンにする
ただ、エラーでないコードにもエラーを出すようになるため、利便性は下がる
code:ts
const xs = 1, 2, 3;
alert(xs3.toFixed(1)); // 不正なコード
// ~~~~~ Object is possibly 'undefined'.
alert(xs2.toFixed(1)); // 正しいコード
// ~~~~~ Object is possibly 'undefined'.
解決策
型に明示的にundefinedを追加する
noUncheckedIndexdAccessよりもこちらの方が、スコープを限定に気にできる点だが、賢さはない
code:ts
const xs: (number | undefined)[] = 1, 2, 3;
alert(xs3.toFixed(1));
// ~~~~~ Object is possibly 'undefined'.
type IdToName = { id: string: string | undefined };
const ids: IdToName = {'007': 'James Bond'};
const agent = ids'008';
// ^? const agent: string | undefined
alert(agent.toUpperCase());
// ~~~~~ 'agent' is possibly 'undefined'.
不健全な型定義
ライブラリにも不健全な型定義が存在する可能性がある
@types/reactのReact.FCは子コンポーネントを受け取ることが理論定期に意味をなさないUIコンポーネントでも、受け取ることが可能になっていた
Object.assignのような歴史的な理由によって間違って片付けされている関数もある
解決策
バグを修正すること
オーグメンテーションや型アサーションを使って回避する
クラス階層における双変性
関数型の代入可能性は共変と反変について考える必要があるので厄介
これはクラスに適応しても当てはまる
code:ts
class Parent {
foo(x: number | string) {}
bar(x: number) {}
}
class Child extends Parent {
foo(x: number) {} // OK
bar(x: number | string) {} // OK
}
class FooChild extends Parent {
foo(x: number) {
console.log(x.toFixed());
}
}
const p: Parent = new FooChild();
p.foo('string'); // 型エラーは出ないが、実行時にクラッシュする
TypeScriptは、メソッドが双変なものとしてモデルングするため、親クラスのメソッドまたは子クラスのメソッドのどちらかが他方に代入可能なら、その継承関係は有効
解決策
strictFunctionTypesにより、単体の関数型はより正確に扱われる
ただ、クラスを継承する際、メソッドのシグネチャが正しくなるよう注意する必要がある
あまり、クラスを継承することが少ないので、意識するシーンは限定的かも
TypeScriptによるオブジェクトと配列の変性の不正確なモデルング
関数の引数を直接変更することは、不健全性の原因になる可能性があるので避ける
変更するつもりがなければ、readonly or Readonly を適応して読み取り専用にする
code:ts
function addFoxOrHen(animals: Animal[]) {
animals.push(Math.random() > 0.5 ? new Fox() : new Hen());
}
const henhouse: Hen[] = new Hen();
addFoxOrHen(henhouse); // なんてこった、鶏小屋(henhouse)に狐(fox)が
function addFoxOrHen(animals: readonly Animal[]) {
animals.push(Math.random() > 0.5 ? new Fox() : new Hen());
// ~~~~ Property 'push' does not exist on type 'readonly Animal[]'.
}
代入可能性とオプションプロパティ
TypeScriptのオブジェクト型は「シール」されていないので、宣言したプロパティ以外のプロパティを持つ可能性がある
詳しくは項目4 構造的型付けに慣れる
上記とオプションプロパティが組み合わさると、不健全性の原因になる可能性がある
code:ts
interface Person {
name: string;
}
interface PossiblyAgedPerson extends Person {
age?: number;
}
const p1 = { name: "Serena", age: "42 years" };
const p2: Person = p1;
const p3: PossiblyAgedPerson = p2;
console.log(${p3.name} is ${p3.age?.toFixed(1)} years old.); // 実行時エラーが発生する
p3への代入で健全性が失われる
本来は型エラーが発生するべきなのに、スルーされてしまう
#TypeScript