💨

Mapをある程度型安全に拡張してみる

に公開

TL;DR

  • Map で has() した直後に get() しても、TS は「undefined | V」のままにする(型が狭まらない)。
  • has() をタイプガード化する(限界はあるけどね)

Map

以下のような Map があるとします。

const map = new Map<string, number>([
  ['a', 1],
  ['b', 2],
]);

この時、map にaがキーがあることを確かめるために has() を使うことができます。

if (map.has('a')) {
  const value = map.get('a');

  if (value !== undefined) {
    console.log(value);
  }
}

if で null チェックをしているので、value の型は number になると思いきや、undefined | numberになります。
そのため undefined を取り除こうとしたら、再度 if でチェックする必要があるので、冗長です。

その回避手段として。has は使わずにいきなり get して、null チェックする方法もありますね。

const value = map.get('a');

if (value !== undefined) {
  console.log(value);
}

ただ、そもそも value に undefined があり得る設計だと区別がつかない。

そこで、もう 1 つの手段として、次のようにhas() をタイプガード化する方法を考えてみます。

interface Map<K, V> {
  has<Key extends K>(key: Key): this is { get(key: Key): V } & this;
}

この Map を使用すると、次のようになります。

if (map.has('a')) {
  const value = map.get('a'); // number
  const value2 = map.get('c'); // 存在しないキーなのでnumber | undefined
}

しかし、Key が リテラル型ではなく、string のような広い型になるとうまくいきません。

const a: string = 'a';
if (map.has(a)) {
  const value = map.get(a); // number
  // cは存在しないのだから本当はstring | undefined になってほしい
}

Key が string のような広い型になると、
get(key: string): V」という強すぎるオーバーロードが足されてしまい、全 string キーが存在するかのように見えてしまいます。

そこでユーティリティの isWide 型を定義して、リテラル型の時だけタイプガードを適用するようにしてみます。

type IsWide<T> = [string] extends [T]
  ? T extends string
    ? true
    : false
  : false;

type NonWide<T> = IsWide<T> extends true ? never : T;

interface Map<K, V> {
  has<Key extends K>(
    key: NonWide<Key> & Key
  ): this is { get(key: Key): V } & this;
  has(key: K): boolean;
}

if (m.has('a')) {
  const v = m.get('a'); // number
}

const c: string = 'c';
if (m.has(c)) {
  const v = m.get(c);
  // v: string | undefined  ← string型では狭まらない
}

けど、これでもまだ完全ではありません。
例えばテンプレートリテラルは上手く機能しません。

type literal = `user:${string}`;
const map = new Map<literal, string>([['user:1', 'Alice']]);
const u: literal = 'user:1';

if (map.has(u)) {
  const g = map.get('user:999');
  // string になってしまう
}

まとめ

Map の型安全性を高めるために、has() をタイプガード化するアプローチがあります。
しかし、不完全なため拡張せず Map を使った方がいいかもしれません。

GitHubで編集を提案

Discussion