🕵️‍♂️

【TypeScript】できるだけ as を避ける理由

2022/11/27に公開約3,000字2件のコメント

型宣言と型アサーション

TypeScriptにおいて、変数に対して型を与える方法は、型宣言as(以下、型アサーション)の2通りがあります。
今回は、型アサーションではなく
型宣言
をできるだけ使うのが良い理由について説明します。

まず簡単に両者の使い方をみてみたいと思います。
以下のようなインターフェースを使用しながら、変数に値と型を代入してみます。

interface Person { name: string };
  • 型宣言
const alice: Person = { name: 'Alice' }; // Type is Person
  • 型アサーション
const bob = { name: 'Bob' } as Person; // Type is Person

どちらもPerson型になっていることがわかります。
上記は変数に対して値と型の両方を与えていますが、型のみを与える場合はどうでしょうか。

  • 型宣言
const alice: Person = {}; 
// Property 'name' is missing in type '{}'
// but required in type 'Person'
  • 型アサーション
const bob = {} as Person; // No error

型宣言ではnameプロパティがないためエラーが吐かれますが、型アサーションではノーエラーとなってしまいます。これは、型宣言は値が型に適合することを保証してくれるものであるのに対して、型アサーションはTypeScriptコンパイラーによる型推論を上書きするものだからです。

少しわかりずらいので、クライアント(tsを用いてプログラムを書く我々)の型に対する知識量の観点から整理してみます。

型宣言において、クライアントは変数に与えられる型について確証があるわけではないと思います。(上記の例でいえば、変数aliceに対して、確実にnameプロパティstring型の値を持ったPerson型が与えられることに確証がない)だからこそ、TypeScriptコンパイラーに対して、「ここにはPerson型が入ってほしいから、そうじゃない場合はエラーのフラグを立ててくれ」と依頼をします。

一方で、型アサーションの場合、クライアントは変数に与えられる型について確証があることが前提にあると思います。つまり、TypeScriptコンパイラーよりもbobに与えられる型について詳しくて、TypeScriptコンパイラーに対して「変数bobには絶対にPerson型が与えられるから、型については気にしなくてもいいよ」と言っているような状態です。だから上記ではTypeSciptコンパイラーはタイプチェック時にエラーフラグを立てません。(ただ、最低限ある条件を満たしている必要があるので、それについては後述します。)

以下のような場合も、型アサーションではエラーになりません。

  • 型宣言
const alice: Person = {
  name: 'Alice',
  occupation: 'JavaScript developer'
}; 
// Object literal may only specify know properties
// and 'occupation' does not exist in type 'Person'
  • 型アサーション
const bob = {
  name: 'Bob',
  occupation: 'JavaScript developer'
} as Person;  // No error

補足

上記の「最低限ある条件」について補足します。
条件とは、アサーションする型が代入する値の型のサブタイプであることです。
なので以下の場合、型アサーション時でもエラーが出ます。

const num = 123;
const str: string = num as string;
// Conversion of type 'number' to type 'string' may be a mistake because 
// neither type sufficiently overlaps with the other. If this was intentional, 
// convert the expression to 'unknown' first.

そして先ほど出てきた以下のような場合、

const bob = {} as Person; // No error

Person型{}型のサブタイプなので通ります。

const num = 123;
const str: string = num as unknown as string; // No error

さらに、TypeScriptにおいてすべての型はunknown型のサブタイプなので、上記のようにすると先ほど出たエラーが解消されます。ただ、エラーを吐くべきところでエラーを吐いてくれないので、この魔術は使ってはいけません。

ではいつ型アサーションを使うべきか

ではどういった場合に型アサーションを使えばいいのでしょうか。
上記にもありますが、それはクライアントがTypeScriptコンパイラーよりも型についてよく知っているときです。具体的には以下のような場合です。

document.querySelector('#myButton').addEventListener('click', e => {
  e.currentTarget // Type is EventTarget
  const button = e.currentTarget as HTMLButtonElement;
  button // Type is HTMLButtonElement
});

TypeScriptコンパイラーはページ上のDOMに対して権限を持たないので、上記の場合、TypeScriptコンパイラーはmyButtonという要素があるかどうか、#myButtonがボタン要素であるかどうかについては全く知りません。一方でクライアントは、変数buttonの型がHTMLButtonElementであることを事前に知っている状態だと思います。こういった場合には、クライアントはTypeScriptコンパイラーよりも型について詳しいので、型アサーションによって型を教えてあげる必要があります。

Discussion

as を避けつつ誤ったコードを書いたときに気付けるようにする方法として、instanceofを使う方法もありますね。

document.querySelector('#myButton').addEventListener('click', e => {
  if (!(e.currentTarget instanceof HTMLButtonElement)) {
    throw new Error("...");
  }
  e.currentTarget // Type is HTMLButtonElement
});

<button><a>を使い分けるシーンがあるなどで、HTML要素でさえあれば良い場合はHTMLElementと書いておくと、<button><a>が切り替わったときにも例外を飛ばさずに済みます。

document.querySelector('#myButton').addEventListener('click', e => {
  if (!(e.currentTarget instanceof HTMLElement)) {
    throw new Error("...");
  }
  e.currentTarget // Type is HTMLElement
});

おっしゃる通りですね。ありがとうございます。

補足として、instanceof自体はvalue spaceの世界における概念(JSランタイムレベルにおけるオペレータ)なので、上記のようなDOM操作をしたい場合等には問題ないことが多いですが、type spaceにおいては使用できない点、注意が必要だと思っています。(例えば、interfaceはtype spaceにおける概念なのでinstanceofに対して指定できません)。

例えば、以下のような場合にエラーになります。

interface Cylinder{
    radius: number;
    height: number;
}

function calculateVolume(shape: unknown) {
    if(shape instanceof Cylinder) {
        shape.radius
        // 'Cylinder' only refers to a type, but is being used as a value here.
    }
}

上記では、CylinderはあくまでTypeオブジェクトであってvalueとしては参照できない、とエラーが出ます。(僕自身最近やっとTypeScriptにおける概念とJSランタイムレベルのおける概念の区別がついてきました...)

ログインするとコメントできます