経緯
4月からフロントエンドエンジニアとして就職し、少しずつ業務にも慣れてきました。業務ではTypeScriptを使い、経験も少しずつ溜まってきた頃、ふと疑問に思いました。
unknownとかneverって、何のために存在してる型なの...?
名前だけは聞いたことがありますが、実際に使ったことはありませんし、どのような意味を持った型なのかもさっぱりでした。
調べてたり、教えてもらっているうちに、自分の中で持っていた認識とは異なることが判明したので、この記事にアウトプットしておこうと思いました。
今回の調査では、サバイバルTypeScriptというサイトが非常に参考になりました。TypeScriptについて細かく、且つ分かりやすくまとめられてありますので、是非ご覧ください。
この記事では、以下の型についてまとめてみます。
- unknown
- never
- undefined
- void
- any
元々の認識
昔の自分と差別化しておくためにも、元々自分が持っていた認識も軽くまとめておきます。以下のような感じでした。
unknown
- 名前の通り、「型が分からない」変数につける型?
- 変数自体は定義されている点がundefinedとの違い?
never
- 絶対に使わないような変数につける型?
- でも使わない変数に型付けても仕方ないような...
undefined
- 変数が定義されていないものにつけられる?
void
- 関数の返り値が存在しない場合につけられる型
- undefinedでもいいのでは?
any
- 何に対しても使える型
- 使わない方がいい程度の認識
このような認識でした。
調査後の認識
続いて、調査後の認識・判明したことをまとめます。
unknown
unknown型は「変数の型が何か分からない」時に使われ、以下の特徴があります。
- 具体的な型の変数に対しては代入できない
let hoge: number = 0;
let fuga: unknown = 1;
hoge = fuga; //error!
- unknown型を適用された変数に対しては、どのような値も代入可能
let hoge: number = 0;
let fuga: unknown = 1;
fuga = hoge; //OK!
- unknown型を適用された変数からのプロパティ・メソッド呼び出しは許可されない
const value: unknown = 10;
value.toFixed(); //error! Object is of type 'unknown'.
- どのような値も代入可能という点で、全ての型のスーパータイプといえる
unknownは「タイプセーフ(型安全)なany型」とよく耳にしますが、それは「unknown型のままではほとんど使うことができない」という特徴がミソです。
メソッドを使うにしても、値を代入するにしても、unknown型のままでは怒られてしまいます。unknown型を使いこなすためには、以下のように型ガードを行うことが求められます。
例えば、unknownでメソッドを呼ぶには以下のように行います。
const value: unknown = 10;
//number型と判明すれば、対応するメソッド呼び出しが可能となる
if (typeof value === "number") {
value.toFixed(); //OK!
}
以上の特徴を踏まえると、unknown型は以下のように使うことができます。
anyな値を安全に利用する
戻り値がanyな値のメソッド(例えば、JSON.parseなど)を利用したりする場合、あらかじめunknownを指定しておくことで、コンパイラが型エラーを表示し、存在しないプロパティのアクセスによる実行時エラーを防げます。
unknownの使い道はよく分かっていませんでしたが、こうして見ると「anyな値を使う場合でも実行時エラーを防げる」という点で十分活用できそうな印象です。
とはいえ、全ての変数・メソッドに明確な型を定義していることが理想だとは思いますが。。。
never
never型は「値を持たない」ことを意味し、以下のような特徴があります。
- 何も代入できない
const foo: never = 1; //error! Type 'number' is not assignable to type 'never'.
const hoge: any = 1;
const fuga: never = hoge; //any型の値も代入不可能です。
const neverVariable: never = 1 as never; //neverは唯一代入できる
- neverはどのような型にも代入できる
const hoge: string = 1 as never; //OK!
const fuga: string[] = 1 as never: //OK!
- neverは最後まで到達できない関数の返り値に設定される
- 関数の途中で絶対にエラーをスローする関数
- 無限ループなどにより、終了することがない関数
//必ずエラーが発生する関数
function throwError(): never {
throw new Error();
}
// 無限ループにより終了できない関数
function forever(): never {
while (true) {} // 無限ループ
}
- 作り得ない型はneverとなる
//numberとstringを一つにまとめることは不可能です
type NumberString = number & string;
- どのような型にも代入可能な点で、neverは全ての型のサブタイプといえる
まとめると、「存在する可能性がない」ものにnever型が設定されます。(という認識です)
neverには以下のような使い方があるそうです。
neverを使った網羅性チェック
neverは「存在する可能性がないものに設定される」という点を利用し、以下のようにユニオン型の網羅性チェックを行うことができます。
type Extension = "js" | "ts" | "json";
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
default: //到達する可能性があるためerror!
const exhaustivenessCheck: never = ext;
break;
}
}
上記の場合、jsonのケースを定義しておらず、defaultが実行される可能性が残されているためにエラーが発生する、といったイメージです。
このようにすることで、Extentionの型が増えたとしても、switchのcaseを追加し忘れないようにできます。
網羅性チェックには「例外」を使おう
網羅性チェックを行う場合、変数を使ってチェックする場合は未使用の変数が必要になってしまう点や、実行時を意識したコードにする点で「例外」を利用した方が望ましいです。
type Extension = "js" | "ts" | "json";
class ExhaustiveError extends Error {
//コンパイル後のコードを見れば、実装の意図が分かる
constructor(value: never, message = `Unsupported type: ${value}`) {
super(message);
}
}
function printLang(ext: Extension): void {
switch (ext) {
case "js":
console.log("JavaScript");
break;
case "ts":
console.log("TypeScript");
break;
case "json":
console.log("JSON");
break;
default:
throw new ExhaustiveError(ext);
}
}
例外を使った網羅性チェックについては、是非こちらをご覧ください。
undefined
undefinedは「値が代入されていない、代入する値が存在しない場合」につけられる型です。
以下のような特徴があります。
- 初期化されていない変数にはundefinedが設定される
let hoge; //初期化されていないのでundefined
- undefinedが明示された場合、undefined以外は代入できない
let hoge: undefined;
hoge = undefined; //OK!
hoge = 1; //error!
undefinedは次のような用途があります。
存在しない可能性のあるプロパティに定義する
undefined単体で使う場面は皆無に等しい印象ですが、numberやstringといった親しみのある型とセットで使われることは多くあると思います。例えば、以下のように型を定義できます。
type User = {
id: number;
name: string;
nickname?: string;
};
プロパティに「?」をつけると、プロパティの型が定義された型+undefinedのユニオン型とされ、省略可能となります。
注意点として、「?」をつけていない場合は省略不可となり、存在しない場合はundefinedを明示する必要があります。
type Hoge = {
optional?: string; //「?」をつけているので、string | undefinedとして扱われる
notOptional: string | undefined;
};
const hoge1: Hoge = {
//「?」をつけていたoptionalプロパティは省略可能
notOptional: "fuga",
};
const hoge2: Hoge = {
optional: "fuga",
notOptional: undefined, //「?」をつけていないので、存在しない場合undefinedを明示する必要がある
};
void
voidは「返す値が存在しない関数の返り値」に設定する型です。
以下のような特徴があります。
- undefinedをvoidに代入することは可能
const hoge: void = undefined; //OK!
- voidをundefinedに代入することは不可能
const hoge: void;
const fuga: undefined = void; //NG!
- 返り値が存在しない関数には、型注釈で返り値にvoid型が適用される
function print(message: string): void {
console.log(message);
}
voidは、基本的に「返り値が存在しない関数の返り値の型」のみに設定されるものです。
ちょっと紛らわしいundefinedとvoidのアレコレ
undefinedとvoidは両者共に「値が存在しない」という点で全く同一のように感じます。実際、僕もそう感じてました。
そこで、改めてvoidとundefinedについて整理してみましょう。
undefinedとvoidは、以下のような型関係にあります。
type A = void extends undefined ? true : false; // false
type B = undefined extends void ? true : false; // true
voidにundefinedを入れることは可能だけど、逆は不可能だということを先述しましたが、それはundefinedがvoidを継承したサブタイプであることが理由です。
また、JavaScriptの特性上、voidを返り値にする関数からはundefinedが値として返されます。
この点から、voidとundefinedは、
「JavaScript上での実体は同じ」だが「TypeScript上での意味合い・利用範囲が異なる」
といえます。
voidは「結果を使わない」ということを明示しているが、undefinedは「結果が存在しない、という結果を使うかもしれない」点で差別化できそうです。
このように考えると、変数にundefinedを指定することはあっても、voidを指定することはまず無いものだと考えて良さそうです。
(voidとundefinedの違いについて、多くの補足をしてくれた同期に感謝...。)
any
皆さんご存知any型です。「どうしても型定義できないような時」に使われます。
次のような特徴があります。
- anyに対してはどのような値も代入可能
const hoge: any = 1;
const fuga: any = "anyだよーー";
- anyは何に対しても代入可能
const hoge: any = [1, 2, 3];
const fuga: string = hoge; //OK!....え??????
any型の大きな特徴は「どのような値も入れられる」というunknown型の特徴と、「何に対しても代入できる」というnever型の特徴を兼ね備えてしまっている点です。つまり、any型は「全ての型のスーパークラス」でありながら、「全ての型のサブクラス」でもあるという、矛盾した存在となっているのです。
できるだけanyは避けたいね
TypeScriptを使う理由として、「型安全に開発を行うことで、実行時に予期せぬエラーが発生する可能性を減らすこと」が挙げられます。
any型は、そんなTypeScriptのメリットを完全に消してしまう型です。TypeScriptを導入して、型安全な開発を目指している以上は、anyを利用することは避けた方が良いでしょう。anyな値を使いたい場合、unknownを明示して安全に開発を行いたいところです。
前述しましたが、any型はnever型とunknown型の両方の特徴を持つ型です。
anyを使いたい...!
使わざるを得ない...!
こんな時こそ、すぐにanyに逃げるのではなく、unknownとneverを上手く使い分けることができれば、より綺麗で堅牢な開発を進められるのではないでしょうか。
とはいえ完全にanyを無くすことは難しく、場合によっては許容する必要もあるかと思います。
anyの考え方について、こちらのページが参考になりましたので、是非ご覧ください。
おわりに
まだまだTypeScriptへの理解が足りないな〜と、今回の調査で思い知りました。
が、今回の調査で新たな知見が増えたのでヨシ!!
最後に、色々補足してくれた同期と先輩に感謝を...。本当にありがとう!
参考文献