0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

オブジェクトの key が存在しなければ、デフォルト値を返すオブジェクト

Last updated at Posted at 2025-03-31

この記事は何か

TypeScript でオブジェクトの存在しないプロパティにアクセスしたときに、undefinedの代わりにデフォルト値を返す関数を実装してみました。
ES2015 のProxyオブジェクトを使って型安全に実装しています。

やりたいコード

const obj = { a: 1, b: 2 };
const defaultObj = withDefaultValue(obj, 0);
console.log(defaultObj.a); // 1
console.log(defaultObj.b); // 2
console.log(defaultObj.c); // 0
console.log(defaultObj.d); // 0

目的

  • 型安全に ??(Nullish coalescing 演算子)を撲滅したい

実装してみた

ES2015 で導入されたProxyオブジェクトを使って実装しました。Proxyを使うと、オブジェクトの基本的な操作をカスタマイズできます。

withDefaultValue.ts
type RecordKey = string | number | symbol; // 型推論のための型
type OrString = string & {}; // 型推論のための型
export const withDefaultValue = <DefaultObject extends Record<RecordKey, unknown>, DefaultValue>(obj: DefaultObject, defaultValue: DefaultValue) => {
  return new Proxy(obj, {
    get(target, prop) {
      return Reflect.get(target, prop) ?? defaultValue;
    },
  }) as DefaultObject & Record<Exclude<keyof DefaultObject, RecordKey> | OrString, DefaultValue>;
};

この実装では、次のことを行っています:

  • getトラップを使って、プロパティアクセスをカスタマイズ
  • Reflect.getで元のオブジェクトからプロパティ値を取得
  • ??演算子で、値がnullundefinedの場合にデフォルト値を返す
  • 型定義で元のオブジェクトの型を保持しつつ、任意のキーに対してデフォルト値の型を返せるように

使い方いろいろ

基本的な使い方

まずは数値をデフォルト値にする例です:

const obj = { a: 1, b: 2 };
const defaultObj = withDefaultValue(obj, 0);

console.log(defaultObj.a); // 1(元のプロパティ値)
console.log(defaultObj.b); // 2(元のプロパティ値)
console.log(defaultObj.c); // 0(デフォルト値)

存在しないプロパティcにアクセスすると、ちゃんとデフォルト値の0が返ってきますね! :santa:

文字列をデフォルト値にする例

次は文字列をデフォルト値にしてみます:

const obj = { a: 1, b: 2 };
const defaultObj = withDefaultValue(obj, "default");

console.log(defaultObj.a); // 1
console.log(defaultObj.b); // 2
console.log(defaultObj.c); // "default"
console.log(defaultObj.d); // "default"

数値以外のデフォルト値も問題なく設定できますね。

オブジェクト内の値をデフォルト値として使用する例

as constを使ってオブジェクトを定義している場合でも、デフォルト値として使え、かつ型推論もうまく機能します。

const obj2 = { a: "default", b: 2, c: 3, d: 4 } as const;
const defaultObj2 = withDefaultValue(obj2, obj2.a);

console.log(defaultObj2.a); // "default"
console.log(defaultObj2.b); // 2
console.log(defaultObj2.c); // 3
console.log(defaultObj2.d); // 4
console.log(defaultObj2.e); // "default"

if 文/switch 文との比較

従来の条件分岐と withDefaultValue を使った実装を比較してみましょう。例として、ユーザーロールに応じた権限チェックを考えてみます。

if 文による実装

const checkPermissionWithIf = (role: string): boolean => {
  if (role === "admin") {
    return true;
  } else if (role === "manager") {
    return true;
  } else if (role === "editor") {
    return true;
  } else {
    return false;
  }
};

switch 文による実装

const checkPermissionWithSwitch = (role: string): boolean => {
  switch (role) {
    case "admin":
      return true;
    case "manager":
      return true;
    case "editor":
      return true;
    default:
      return false;
  }
};

withDefaultValue を使った実装

const permissions = {
  admin: true,
  manager: true,
  editor: true,
} as const;

const permissionMap = withDefaultValue(permissions, false);

const checkPermissionWithProxy = (role: keyof typeof permissionMap): boolean =>
  permissionMap[role];

比較結果

実行結果はどれも同じですが、withDefaultValue を使った実装には以下のメリットがあります:

  1. コードの簡潔さ: 条件分岐が減り、コードがすっきりします
  2. メンテナンス性: 権限の追加・削除が単にオブジェクトのプロパティを編集するだけで済みます
  3. 宣言的プログラミング: 「何をするか」を中心に記述できます
  4. 型安全性: TypeScript の型推論が効きやすくなります
  5. 再利用性: オブジェクトを型に利用できる(as const との相性がいい)

特に、条件が多い場合やデフォルト値を何度も使う場合に、withDefaultValue の利点が顕著になります。

エディターでの補完

TypeScript の型推論が効くので、エディターでの補完も効きます。

スクリーンショット 2025-03-31 15.27.43.png

実装の解説

Proxy オブジェクトについて

Proxyオブジェクトは、オブジェクトの基本的な操作をカスタマイズするための仕組みです。

const proxy = new Proxy(target, handler);
  • target: 元のオブジェクト
  • handler: トラップを定義したオブジェクト

今回はgetトラップのみを使いましたが、他にもsethasdeletePropertyなど様々なトラップがあります。

Reflect オブジェクトについて

Reflectは、Proxy と対になる組み込みオブジェクトで、Proxy ハンドラーメソッドと同じ名前と引数を持つメソッドを提供します。

// これよりも
const value = target[prop] ?? defaultValue;

// こちらのほうが安全です
const value = Reflect.get(target, prop) ?? defaultValue;

Reflect.getを使うと、元のオブジェクトのプロパティアクセスをより安全に行えます。プロパティが Symbol だったり、getter だったりする場合に便利です。

Nullish coalescing 演算子 (??)

??演算子は、左辺がnullまたはundefinedの場合に右辺の値を返す演算子です。

const value = someValue ?? defaultValue;

これは||演算子と似ていますが、||演算子は左辺が falsy (0, '', false, null, undefined, NaN) のときに右辺を返すのに対して、??演算子は左辺がnullまたはundefinedのときだけ右辺を返します。

今回のようなデフォルト値の実装では、0''falseなどの値も有効な値として扱いたいので、??演算子のほうがより範囲が適切です。

TypeScript の型定義について

型定義の部分を詳しく見てみましょう:

<DefaultObject extends Record<RecordKey, unknown>, DefaultValue>(obj: DefaultObject, defaultValue: DefaultValue) => {
  // ...
} as DefaultObject & Record<Exclude<keyof DefaultObject, RecordKey> | OrString, DefaultValue>;

ここでは:

  1. DefaultObjectは元のオブジェクトの型
  2. DefaultValueはデフォルト値の型
  3. 返り値の型はDefaultObject & Record<Exclude<keyof DefaultObject, RecordKey> | OrString, DefaultValue>

この型定義により、元のオブジェクトのプロパティはその型を保持しつつ、存在しないプロパティにアクセスした場合はデフォルト値の型DefaultValueとして扱われます。これによって型安全なコードが書けるようになります。

やってみての感想

この実装を使うと、オブジェクトのプロパティにアクセスする際に毎回 obj.prop ?? defaultValue のように書く必要がなくなり、コードがすっきりします。特に、同じデフォルト値を何度も使う場合には便利ですね!

TypeScript の型定義もしっかりしているので、型推論もうまく機能します。エディタの補完も効くので、実用的だと思います。

Proxyオブジェクトはブラウザのサポートも良くなってきていますが、古いブラウザをサポートする必要がある場合は polyfill が必要かもしれません。

まとめ

3 行で要約すると:

  1. TypeScript で存在しないプロパティにアクセスしたときにデフォルト値を返す関数を実装した
  2. Proxyオブジェクトを使って、オブジェクトのプロパティアクセスをカスタマイズした
  3. 型定義もしっかり行い、型安全なコードを実現した

みなさんもぜひ試してみてください!何か改善点や質問があればコメントください。

現場からは以上です:fish:

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?