未だに TypeScript に自信が持てないので調べてみた。 → ユーザー定義型ガード、asで書くかanyで書くか - uhyo/blog にもっと良い説明があります。
もっと良い案をご存知であれば是非教えて下さい。
TypeScript で JSON のように柔軟なデータを読み込もうとするとデータの実行時チェックに苦労します。例えばこういうデータがある時:
const item: unknown = {
name: "ククルス ドアン",
};
unknown
とは外部から来るデータに割り当てる特殊な型です。実際には API 呼び出しの結果などで外部から来ると思って下さい。このデータを利用するには自分で型を付ける必要があります。例えばこんな型:
type Person = {
name: string;
};
型を付けるのに間違った方法は as で強制する事です。
console.log(`間違い: ${(item as Person).name}`);
こうするとたまたまデータが正しければ動きますが、null や undefined が来ると実行時エラーになります。エラーを防ぐには、データを利用するデータが型に合っているか確認する必要があります。こんな当たり前の事が結構 TypeScript は難しい。
マシな方法
色々調べてみて、マシな方法は type guard を使う事と分かりました。
const isNotNullish = (data: unknown): data is Record<string, unknown> => data != null;
if (isNotNullish(item) && typeof item.name === "string") {
const person: Person = { name: item.name };
console.log(`recommended: ${person.name}`);
}
この、isNotNullish(item) && typeof item.name === "string"
というのがデータの型を確認する部分です。
-
isNotNullish(item)
で item にプロパティアクセスが出来るか確認します。-
isNotNullish
の実装はdata != null
なので、プロパティの無いnull
とundefined
を避ける事が出来ます。
-
-
typeof item.name === "string"
でname
プロパティの型を確認します。
欲しかった方法
これにたどり着く前に、
if (typeof item?.name === "string") { // Property 'name' does not exist on type 'unknown'.
const person: Person = item;
console.log(`"name" in item で判別: ${person.name}`);
}
のように書けたら良いなと思っていました。?.
は「オプショナルチェーン」と言って、レシーバにプロパティが無くてもエラーにならず undefined を返す演算子です。残念ながら Typescript の unknown には使えないようです。ではせめて:
if ("name" in item && typeof item.name === "string") {
const person: Person = person;
console.log(`"name" in item で判別: ${person.name}`);
}
はどうでしょう? typeof item.name
の前にちゃんと name
プロパティの存在確認をしているので大丈夫な気がします。しかし残念ながら Javascript では "name" in null
や "name" in undefined
や "name" in 42
などプリミティブと組み合わせると実行時エラーになるのでした。ならばその前に name がプリミティブである可能性を排除すれば良いのではと:
if (
typeof item === "object" &&
item != null &&
"name" in item &&
typeof item.name === "string" // Property 'name' does not exist on type 'object'.
) {
const person: Person = item;
console.log(`全部入り判別: ${person.name}`);
}
のように考えられる限り全ての条件を入れても無駄でした。Typescript は "name" in item
のような式をプロパティの存在判定に使ってくれず、コンパイルエラーになります。
Type Guard を使う
というわけで上記マシな方法がなぜマシなのか再度見ます。
const isNotNullish = (data: unknown): data is Record<string, unknown> => data != null;
if (isNotNullish(item) && typeof item.name === "string") {
const person: Person = { name: item.name };
console.log(`recommended: ${person.name}`);
}
ここでは isNotNullish
という Type Guard を定義しています。Type Guard とは boolean を返す関数で、戻り型を 引数 is 型
のような特殊な記法で書きます。Type Guard は Typescript の型判定がうまく動かない時に手助けをします。ここでは data is Record<string, unknown>
のように指定して、もしも結果が true ならプロパティアクセスが可能である事を TypeScript に伝えています。
Javascript では data != null
の時。つまり data !== null && data !=== undefined
の時には必ずプロパティアクセスが可能になるので、item.name
がエラーにならなくなります。
この item を Person 型にするために、本当は
const person: Person = item.name; // Property 'name' is missing in type 'Record<string, unknown>' but required in type 'Person'
のように直接代入したかったのですが、typeof item.name === "string"
があるにも関わらずエラーになります。もし直接代入したければもう一つ Type Guard を使って
const isNotNullish = (data: unknown): data is Record<string, unknown> => data != null;
const isPerson = (data: unknown): data is Person => isNotNullish(data) && typeof data.name === "string";
if (isPerson(item)) {
const person: Person = item;
console.log(`recommended: ${person.name}`);
}
のようにも出来ますが、Type Guard 内のバグを Typescript は見つけられないので出来るだけ少なくしたいです。
参考
-
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/No_properties
- Javascript でプロパティにアクセス出来ない値は null と undefined です。
- 42 などのプリミティブもプロパティがありませんが何故か実行時エラーにはなりません。
- ユーザー定義型ガード、asで書くかanyで書くか - uhyo/blog
-
typescript - Narrowing an object of type unknown - Stack Overflow
- 似たようなアイデアだがもっとややこしい。
-
TypeScript: Documentation - Narrowing
- in オペラータは Union の型判定には使えます。
-
Allow more constructs to work as type guards for
unknown
· Issue #25720 · microsoft/TypeScript · GitHub- in を unknown の型推測に使ってほしいという要望。
-
オプショナルチェーン (?.) - JavaScript | MDN
- 本当はこの機能が使いたかった。
-
Optional Chaining and Type Narrowing · Issue #33736 · microsoft/TypeScript · GitHub
- オプショナルチェーンによる型推測の案は却下されています。
-
TypeScript の Type Guard を使ってキャストいらず - Qiita
- unknown が無い時代に書いた Type Guard のメモ。any を使ってるので参考にしないで下さい。