anyとunknown

現在こそTypeScriptはそれなりの地位を得て、多くのnpmで公開されているパッケージは最初からTypeScriptで作られていたり、第三者によって型定義ファイルが提供されることも増えましたが、いまだに型定義ファイルを持たないパッケージもあります。

JavaScriptをTypeScriptの世界に招くにあたり必須となるのはずばり型です。ですが前述のとおりJavaScriptとしてのみ公開されているパッケージには型の定義ファイルがないものもあります。

それらは型定義ファイルが公開されるまで使うことができないかと言うとそうではありません。若干TypeScriptとしての恩恵を捨てることにはなりますが、まったく使えないよりはいいでしょう。

型が不定のとき、TypeScriptではanyまたはunknownという型を使います。

const whatIsIt: any = superElegantPackage.doesFirst();
const whatIsIt: unknown = superElegantPackage.doesSecond();

これらの型の使い方について説明をします。

any, unknownについて

any, unknown型はどのような値も代入できます。

const any1: any = null;
const any2: any = undefined;
const any3: any = true;
const any4: any = 0.8;
const any5: any = 'Comment allez-vous';
const any6: any = {
x: 0,
y: 1,
name: 'origin'
};
const unknown1: unknown = null;
const unknown2: unknown = undefined;
const unknown3: unknown = true;
const unknown4: unknown = 0.8;
const unknown5: unknown = 'Comment allez-vous';
const unknown6: unknown = {
x: 0,
y: 1,
name: 'origin'
};

ちなみに逆の概念としてどの値も代入できないneverという型もありますが、今回は説明を省きます。

any型に代入したオブジェクトのプロパティ、メソッドは使用することができます。

console.log(any4.toFixed());
// -> 1
console.log(any5.length);
// -> 18
console.log(any6.name);
// -> 'origin'

一方、unknown型に代入したオブジェクトのプロパティ、メソッドは使用することができません。使用できないどころか、実行することができません。

console.log(unknown4.toFixed());
// Object is of type 'unknown'.
console.log(unknown5.length);
// Object is of type 'unknown'.
console.log(unknown6.name);
// Object is of type 'unknown'.

これだけ見るとunknown型よりもany型の方が優れていると思われるかもしれませんがそうではありません。any型は言い換えればTypeScriptが型のチェックを放棄した型であり、そのためなんでもできます。any型を使うということはTypeScriptでせっかく得た型という利点を手放しているのと同じです。

これでは存在しているエラーはコンパイル時には気が付けず、ソフトウェアをリリースしたあと実際のユーザーが使ったときの実行時エラーとなります。それが不具合報告や、クレームとなり、被害が拡大していきます。

any型に関しては、次のような無茶なコードもTypeScriptは一切関与せず、実行してみてプログラムが実行時エラーになる、初めてこのプログラムが不完全であることがわかります。

console.log(any6.x.y.z);
// Cannot read property 'z' of undefined

unknown型は一貫してTypeScriptがプロパティ、メソッドへのアクセスを行わせません。そのため実行することができず、意図しないランタイム時のエラーを防止します。

console.log(unknown6.x.y.z);
// Object is of type 'unknown'.

TypeScriptのプロジェクトを作る時に必要なtsconfig.jsonにはこのany型の使用を防ぐためのオプションとしてnoImplicitAnyがあります。既存のJavaScriptのプロジェクトをTypeScriptに置き換えていくのではなく、スクラッチの状態からTypeScriptで作るのであればこの設定を入れるとよいでしょう。

tsconfig.jsonにある他の厳格なコーディングのための設定の説明もありますので併せて参照してください。

型アサーションに使う

次のクラスを例に考えます。

class Mammal {
}
class Cat extends Mammal {
public miaow(): void {
console.log('miaow miaow');
}
}
class Bird {
}
class Duck extends Bird {
public quack(): void {
console.log('quack quack');
}
}

一般的な言語でいうところのキャストはTypeScriptでは型アサーションと言い、asという構文を使います。 TypeScriptではアップキャストは問題なくできます。これは他の言語でも同じだと思います。

const duck: Duck = new Duck();
const bird: Bird = duck as Bird;

ダウンキャストに関してもTypeScriptはこの型アサーションを使えば問題なくできます。 型アサーションを使うことによる問題はすべてプログラマが対処する必要があります。

const bird: Bird = new Bird();
const duck: Duck = bird as Duck;
duck.quack();
// duck.quack is not a function

ところが、まったく関係のないクラスへの型アサーションはできません。

const cat: Cat = new Cat();
const duck: Duck = cat as Duck;
// Conversion of type 'Cat' to type 'Duck' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
// Property 'quack' is missing in type 'Cat' but required in type 'Duck'.

このような時にどうしても無理やり型アサーションをしたい時は一度any, unknown型を経由して本来欲しいクラスに型アサーションします。

const cat: Cat = new Cat();
const duck1: Duck = cat as any as Duck;
const duck2: Duck = cat as unknown as Duck;

ただし、このときに発生しうる問題についてはダウンキャスト同様にプログラマが対処しなければなりません。