2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unknown にアクセスしたい。- TypeScript で外部データを型安全に読み込む。

Last updated at Posted at 2022-04-27

未だに 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 なので、プロパティの無い nullundefined を避ける事が出来ます。
  • 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 は見つけられないので出来るだけ少なくしたいです。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?