📘

Typescriptのunknown型について

2023/02/09に公開

unknown型とは

unknown型は型が不定の時に使用する型です。Typescript3.0で導入されました。
例えば、色を表す変数では、色の名前を直接使用する場合やRGB毎に色を指定する場合や直接数値で表す場合のいずれでも値を代入することができます。

let color: unknown;
color = "white"; // OK
color = { red: 255, green: 255, blue: 255 }; // OK
color = 16581375; // OK

any型との違い

しかし、そのような型はany型でも同様のことができます。

let color: any = "white"; // OK
color = { red: 255, green: 255, blue: 255}; // OK
color = 16581375; // OK

それではなぜunknown型という型ができたのでしょうか?
any型はどのような型の変数にも代入ができます。

const value: any = 10;
const numValue: number = value;
const strValue: string = value;
console.log(strValue.toUpperCase());

コンパイルしてもコンパイルエラーが出ずjavascriptに変換できます。

var anyValue = 10;
var intValue = anyValue;
var strValue = anyValue;
console.log(strValue.toUpperCase());

実行してみると、strValueの値は数値の10なのでtoUpperCase()メソッドは存在しておらずランタイムエラーが発生します。

TypeError: strValue.toUpperCase is not a function
    at Object.<anonymous> (/project/home/user/workspace/index.js:13:22)
    at Module._compile (node:internal/modules/cjs/loader:1126:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
    at Module.load (node:internal/modules/cjs/loader:1004:32)
    at Function.Module._load (node:internal/modules/cjs/loader:839:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

any型はコンパイル時にエラーが出ませんでした。型安全ではありません。

unknown型ではこのようなことを行うとコンパイル時にエラーが発生します。

const unknownValue: unknown = 10;
const intValue: number = unknownValue;
const strValue: string = unknownValue;
console.log(strValue.toUpperCase());
index.ts:12:7 - error TS2322: Type 'unknown' is not assignable to type 'number'.

12 const intValue: number = unknownValue;
         ~~~~~~~~

index.ts:13:7 - error TS2322: Type 'unknown' is not assignable to type 'string'.

13 const strValue: string = unknownValue;
         ~~~~~~~~


Found 2 errors in the same file, starting at: index.ts:12

unknown型は特定の型には代入できないのです。
また数値を代入したからといって、数値のメソッドを呼び出すこともできません。

const unknownValue: unknown = 10;
console.log(unknownValue.toFixed());
index.ts:12:14 - error TS2339: Property 'toFixed' does not exist on type 'unknown'.

12 unknownValue.toFixed();
                ~~~~~~~


Found 1 error in index.ts:12

unknown型の利用方法

それだけではunknown型の意味はないのですが、型ガードにより型を絞り込んで使用することができます。

const unknownValue: unknown = 10;
if (typeof unknownValue === "number") {
  console.log(unknownValue.toFixed());
}

if文のtypeofによりunknownValueの型がnumberの時に絞られたため、toFixed()メソッドを呼び出すことができます。

型ガードを関数にしてみます。

function isNumber(value: unknown): boolean {
  return typeof value === "number";
}

const unknownValue: unknown = 10;
if (isNumber(unknownValue)) {
  console.log(unknownValue.toFixed());
}

コンパイルをするとコンパイルエラーが発生します。

index.ts:7:28 - error TS2339: Property 'toFixed' does not exist on type 'unknown'.

7   console.log(unknownValue.toFixed());
                             ~~~~~~~


Found 1 error in index.ts:7

型ガードが効いていないようです。関数で型ガードを利かすためにはisを使用します。ユーザー定義型ガードというそうです。関数の結果がtrueの場合はisで指定した型に絞り込みます。

function isNumber(value: unknown): value is number {
  return typeof value === "number";
}

const unknownValue: unknown = 10;
if (isNumber(unknownValue)) {
  console.log(unknownValue.toFixed());
}

コンパイルはエラーなく終了しました。

プリミティブな型の場合は型ガード関数を作成するのは簡単ですが、オブジェクト型の場合はオブジェクトであって、オブジェクトのプロパティの型も検証しなくてはなりません。
zodというバリデーションライブラリーがあるので、それを使用した方がいいでしょう。

type User = {
  login: string,
  name: string
};
function isUser(value: unknown): value is User {
  if (typeof value !== "object") {
    return false;
  }
  const user = value as Record<keyof User, unknown>;
  if (typeof user.login !== "string") {
    return false;
  }
  return typeof user.name !== "string";
}

TypescriptのIssueで言及されているようにJSON.parse()はany型を返しますが、unknown型で受け取って存在しないプロパティにアクセスしたときに発生するエラーを実行時ではなくコンパイル時に見つけることができるようにします。

まとめ

unknown型とany型の違いが理解できていなかったのですが理解できました。
unknown型では型ガードをしなければ変数を使用することができません。
any型では実行時にエラーが発生するものがunknwon型ではコンパイル時にエラーが発生するため、コードの間違いに早く気付くことができます。

Discussion