dtaniguchi.com

blog - TypeScript

Vue3 と Apple MapKit JS Ver.5 ② – Singleton Instance の Mutex

d.taniguchi

2024年8月15日 23:35

Post / コメント

  • AppleMaps
  • ChatGPT
  • Tips
  • TypeScript
  • Vue3

前回の続きである。

前回は地図を使用する UI 開発で、 Apple Maps の地図上に表示する Marker コンポーネントを、 Vue3 とどのように組み合わせて実装し、 Storybook に追加したかを書いた。

今回は、地図 UI を取り扱ういくつかのコンポーネントが、最初に呼び出す Apple Maps の SDK Object を、フレームワークとどの様に組み合わせればいいのかについて話をする。

Apple Maps ではまず、 Web ブラウザで地図表示を行うために必要な部品一式が実装されている、 MapKit JS と呼ばれる SDK Object の読み込みを済ませておく必要があった。具体的には公式で公開されている以下のコードである。

// This promise resolves when the browser finishes downloading and evaluating MapKit JS.
const mapKitJsLoadedPromise = new Promise(resolve => {
    const element = document.createElement("script");
    element.addEventListener("load", resolve, { once : true });
    element.src = "https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"
    element.crossOrigin = "anonymous";
    document.head.appendChild(element);
});

このコード、SDK Object が定義されている js ファイルを動的制御で読み込む場合に使用する。読み込み処理自体は非同期だが、完了するまで次の処理を進めることができないため、実質は直列処理になる。この方法は MapKit JS を Full bundle でしか読み込めないため、処理速度の観点から公式で本番環境非推奨となっている。

普通 js ファイル読み込みには、以下のような script タグを head タグ内に宣言する方法をとる。

<script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js"
    crossorigin async
    data-callback="initMapKit"
    data-libraries="services,full-map,geojson"></script>

この場合、js ファイルの読み込み順序がフレームワークの読み込みより先に実行されるよう制御できる。複数のコンポーネントがバラバラな順序で SDK Object へアクセスしても、 Instance が存在しない可能性を考慮する必要がないし、Minimum bundle のライブラリを指定できるため、読み込み時間を短縮できる。

しかし今回は、以下の理由から動的制御で直列処理による SDK Object の読み込みが最善だと判断していた。

  • 特定の Map サービスに依存しない UI 設計を実現しているため、 DI するオブジェクトの差し替えで Map サービスの切り替えが可能な、ベンダーロックインしない構成が望ましい
  • 可読性や可変性維持のため疎結合と凝集度の向上を実現するには、ユーザーが地図情報にアクセスしたタイミングで、関連するライブラリの読み込みが実行される実装が最善
  • MapKit JS は Full bundle でも十分高速に読み込み可能で、ライブラリ使用直前の直列処理でも UX は悪化しない
  • 地図サービスによって、どのタイミングで費用が発生するか分からないため、ユーザーが地図機能を使用しなかった場合、費用が発生する可能性そのものが生じない設計こそ、懐とリードエンジニアの心理的安全に優しい

これらを踏まえると、実装は以下のようになった。(記事の趣旨に合わせて一部省略)

/lib/map/index.d.ts
interface NavigationMap {
  launch(latLng: LatLng): Promise<void>
  // 他の地図制御メソッド 省略
  dispose(): void
}
  
interface MarkerIndicationMap {
  launch(latLng: LatLng): Promise<void>
  // 省略
  dispose(): void
}
/lib/map/apple-maps/index.ts
import AppleNavigationMap from "./navigation-map"
import AppleMarkerIndicationMap from "./marker-indication-map"

export const getNavigationMap: (element: HTMLElement) => NavigationMap = 
  (element: HTMLElement) => new AppleNavigationMap(element)

export const getMarkerIndicationMap: (element: HTMLElement) => MarkerIndicationMap =
  (element: HTMLElement) => new AppleMarkerIndicationMap(element)
/lib/map/apple-maps/apple-maps.ts
type Mapkit = any

export default abstract class AppleMaps {

  private static mapKit: Mapkit

  protected static async getMapKit(): Promise<Mapkit> {

    if (this.mapKit) {
      return this.mapKit
    }

    await new Promise(resolve => {
      const element = document.createElement("script")
      element.addEventListener("load", resolve, { once : true })
      element.src = "https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"
      element.crossOrigin = "anonymous"
      document.head.appendChild(element)
    })

    this.mapKit = await new Promise((resolve, reject) => {
      const mapKit = (globalThis as any).mapkit

      mapKit.init({
        // libraries: ["services", "full-map", "annotations"],
        authorizationCallback: function(done: any) {
          done("-- TOKEN --")
        },
        language: "ja"
      })

      mapKit.addEventListener("configuration-change", (event: any) => {
        switch (event.status) {
          case "Initialized":
            resolve(mapKit)
            return
          // case "Refreshed":
          // default:
        }
        reject(event)
      })

      mapKit.addEventListener("error", (error: any) => reject(error))
    })

    return this.mapKit
  }
}
/lib/map/apple-maps/marker-indication-map.ts
import AppleMaps from "./apple-maps"

export default class AppleNavigationMap extends AppleMaps implements NavigationMap {

  private element: HTMLElement
  private map: any = undefined

  constructor(element: HTMLElement) {
    super()
    this.element = element
  }

  private async getMap() {
    if (this.map) {
      return this.map
    }
    const mapKit = await AppleMaps.getMapKit()
    this.map = new mapKit.Map(this.element, {
      // 省略
    })
    return this.map
  }

  async launch(latLng: LatLng) {
    const mapKit = await AppleMaps.getMapKit()
    const map = await this.getMap()
    map.region = new mapKit.CoordinateRegion(
      // 省略
    )
  }

  // 他の地図制御メソッド 省略

  dispose() {
    this.map?.destroy()
  }
}
/lib/map/apple-map/navigation-map.ts
// marker-indication-map.ts と同様

最初の Interface は、地図 UI を担当するコンポーネントから呼び出すことを想定している。コンポーネントの分だけ定義され、地図の制御に必要なメソッドを用意しておく。地図制御に係る Map ベンダー固有のコードを、コンポーネントの実装から分離できるため、DI の設定次第で異なる Map ベンダーに切り替えることが可能になる。

そして、地図ベンダーのサービスに依存するコードをまとめる Concrete class に続く。

Abstract class を定義、ここに SDK Object である MapKit の Getter を用意する。この Getter には MapKit の Instance 生成処理も含み、 最初のアクセスが来るまで Instance 生成を保留する。 Lazy initialization だ。一度生成された Instance はその後ずっと再利用される。

継承する class からしか使用しない関数なので、アクセス修飾子は Protected 、Singleton instance なので、static を指定する。

特段問題なさそうに見える。試しに動かしてみると、以下の Warning が出た。

[MapKit] Mapkit namespace already exists; did you import MapKit more than once?

嫌な予感はあった。問題の発生箇所はやはり Abstract class の getMapKit メソッドだった。SDK Object の読み込み処理が複数回実行されている。

Singleton にしたい SDK Object の Instance 生成に時間を要すため、生成完了より前に別のコンポーネントの表示制御が、別スレッドで生成処理をキックしてしまう。getMapKit メソッドを Mutex にしなければ Singleton を保証できないのだ。

しかし、どの様なコードを書けばいいのだろうか?

例えば、 Java であれば Lazy initialization には定番の書き方がある。 synchronized を使う方法がすぐに思い浮かぶ。

Java
abstract class AppleMaps {

  private static MapKit mapKit;

  static synchronized MapKit getMapKit() {
    if (mapKit != null) {
      return mapKit;
    }
    // MapKit 生成処理
  }
}

Kotlin に至っては Syntax が存在する。

Kotlin
abstract class AppleMaps {

  protected companion object {
    val mapKit by lazy {
      // MapKit 生成処理
    }
  }
}

Java や Kotlin であれば上記のコードで問題は解決する。

しかし JavaScript でスレッド制御や Lazy initialization に関する Syntax は聞いたことがない。Promise を使えば作れそうな気もするが、少し考えただけではコードを思いつくことができなかった。 Google 検索で実現方法を調べてみる。

検索で上位にでてきた記事は、 Third-party 製のライブラリ紹介だった。主に 2 つが紹介されている。

これらは、JavaScript で Semaphore や Mutex を使用する場合、よく使用される Package のようだ。試しにプロジェクトに追加してみると動作しない。いずれもソースコード内で process object を使用しており、 Vite を使用している Project で使用する場合、単純な Package 追加だけでは動作しないことが分かった。

実際に問題なく動作する Mutex の実装を掲載しているページを発見することができたのは、調査をはじめてしばらく経過してからだった。記事の内容を基にテストすると、必要な振る舞いをすることが確認できた。

Danny Kim / Simple TypeScript Mutex Implementation
https://dev.to/0916dhkim/simple-typescript-mutex-implementation-5544

ひとまず、設計の見直しは免れたようである。

動作したのはいいのだが、ソースコードを読むと Semaphore をシングルスレッドで動作させることで Mutex を実現しているように見える。実装がやや複雑で、何をしているのかすぐに理解することが難しい。記事の基になっている Package を追加して使う前に、必要なコードだけ抜き出して振る舞いを理解しようとした時、ふと ChatGPT の存在を思い出した。最近、変数の命名で迷った時、相談に来ていたのだ。ChatGPT に Mutex を質問したら、どの様な返答をしてくれるだろう。

ChatGPT が回答した TypeScript による Mutex の実装と使用例
class Mutex {
  private mutex = Promise.resolve();

  async lock<T>(fn: () => Promise<T>): Promise<T> {
    let release: () => void;
    const releasePromise = new Promise<void>(resolve => release = resolve);

    // finally の代わりに then と catch を使ってロックを解放
    const result = this.mutex.then(() => fn())
      .then(
        (value) => {
          release(); // 正常終了時にリリース
          return value;
        },
        (error) => {
          release(); // エラー時にも必ずリリース
          throw error;
        }
      );

    this.mutex = releasePromise;

    return result;
  }
}

const mutex = new Mutex();

async function criticalSection(id: number) {
  console.log(`Task ${id} is waiting to enter the critical section.`);
  await mutex.lock(async () => {
    console.log(`Task ${id} has entered the critical section.`);
    // クリティカルセクションに入った後、非同期の処理をシミュレート
    await new Promise(resolve => setTimeout(resolve, 1000)); 
    console.log(`Task ${id} is leaving the critical section.`);
  });
}

async function runTasks() {
  await Promise.all([
    criticalSection(1),
    criticalSection(2),
    criticalSection(3)
  ]);
}

runTasks();

1 箇所 ES2018 以降でないと使用できない Syntax が含まれていたが、一発で正解を出してきた。これこそ、欲しかった答えだった。

ChatGPT のコードは Promise を 2 つ、巧みに組み合わせることでスレッド制御を行い、 Mutex を実現していた。シンプルで可読性の高いコードである。これをプロジェクトに採用することにした。修正した getMapKit メソッドは以下のようになった。

/lib/map/apple-maps/apple-maps.ts
type Mapkit = any

export default abstract class AppleMaps {

  private static mapKit: Mapkit
  private static mutex = new Mutex()

  protected static async getMapKit(): Promise<Mapkit> {
 
    await this.mutex.lock(async () => {

      if (this.mapKit) {
        return
      }
      
      await new Promise(resolve => {
        const element = document.createElement("script")
        element.addEventListener("load", resolve, { once : true })
        element.src = "https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"
        element.crossOrigin = "anonymous"
        document.head.appendChild(element)
      })
  
      this.mapKit = await new Promise((resolve, reject) => {
        const mapKit = (globalThis as any).mapkit;
        (globalThis as any).mapkit = undefined
        
        mapKit.init({
          // libraries: ["services", "full-map", "annotations"],
          authorizationCallback: function(done: any) {
            done("-- TOKEN --")
          },
          language: "ja"
        })
        
        mapKit.addEventListener("configuration-change", (event: any) => {
          switch (event.status) {
            case "Initialized":
              resolve(mapKit)
              return
            // case "Refreshed":
            // default:
          }
          reject(event)
        })
        
        mapKit.addEventListener("error", (error: any) => reject(error))
      })
    })
    
    return this.mapKit
  }
}

ChatGPT への質問は Google 検索と比較して、どのぐらい効率よく答えにたどりつけたのだろう。このテーマに関する Google 検索の成果は、あまり芳しいものではなかった。欲しい答えの一部にたどり着いた段階で、かなりの時間と労力を費やしていた。

キーワードから関連しそうなネット記事の候補を一覧表示するだけなのか、疑問に対してそのものズバリの答えを返すのか、この差を考えれば当然の結果ではある。

なんということだろう。今回、技術的な調査で最初に用いるソリューションを更新することになった。ググるという言葉は、もう過去のものになろうとしている。

Vue Ver.3.4 Release と DevTools

d.taniguchi

2024年4月10日 9:34

Post / コメント

  • TypeScript
  • Vue3

昨年末である 2023/12/28 に、 Vue の Ver.3.4 が Releaseとなった。

The Vue Point “Announcing Vue 3.4”
https://blog.vuejs.org/posts/vue-3-4

Template Parser の全面書き換えによる 44% の SFC コンパイル速度向上や、状態監視の改良により、computed、 effect、 watch、 watchEffect 各関数の呼び出しの実行が、実値更新時のみになるといったパフォーマンス上の改善もさることながら、コーディング上のうれしい改善も 2 つ含まれていた。

defineModel マクロの stable 格上げと、v-bind の shorthand 追加だ。いずれも欲しかった Syntax である。簡単なコンポーネントでどの程度差が出るか比較する。

Vue 3.3 までのテキスト入力コンポーネント

InputText.vue
<script setup lang="ts">
import { type HTMLAttributes } from "vue"

const props = withDefaults(defineProps<{
  modelValue?: string
  placeholder?: string
  disabled?: boolean
  inputmode?: HTMLAttributes["inputmode"]
}>(), {
  modelValue: "",
  placeholder: "入力してください。",
  inputmode: "text",
})

const emit = defineEmits<{
  (e: "update:modelValue", value: string): void
}>()
</script>

<template>
  <input
    type="text"
    v-model="modelValue"
    :placeholder="placeholder"
    :disabled="disabled"
    :class="{ disabled: disabled }"
    :inputmode="inputmode"
    autocomplete="off"
  />
</template>

Vue 3.4 の defineModel と shorthand を使用したテキスト入力コンポーネント

InputText.vue
<script setup lang="ts">
import { type HTMLAttributes } from "vue"

const text = defineModel({ default: "" })

const props = withDefaults(defineProps<{
  placeholder?: string
  disabled?: boolean
  inputmode?: HTMLAttributes["inputmode"]
}>(), {
  placeholder: "入力してください。",
  inputmode: "text",
})
</script>

<template>
  <input
    type="text"
    v-model="text"
    :placeholder
    :disabled
    :class="{ disabled: disabled }"
    :inputmode
    autocomplete="off"
  />
</template>

可読性の改善は著しい。

現在、私が開発を進めているプロジェクトは昨年の 9 月から始めたものだから、 defineModel はまだ実験段階だった。忸怩たる思いで modelValue の defineEmits を書いていたのだが、どこかのタイミングで defineModel に修正したい。

さて、新機能の動作確認当たり Vue Ver 3.4 の初期プロジェクトを Scaffolding tool で生成しようとした際、見慣れない文字列を見つけた。

実験段階の DevTools なるもののインストール確認項目が追加さたようだ。これは非常に興味深い。試しにこれも追加して、開発サーバーを立ち上げたら以下のような画面が表示された。

一見いつもの初期プロジェクトだが、中央下に小さなアイコンがある。これが追加された DevTools らしい。Vue アイコンをクリックすると以下の画面が出てきた。

新しい DevTools はブラウザの拡張機能ではなく、開発中プロジェクトのページ上に表示されるらしい。なるほどと思い次の項目を選択して驚愕した。

現在ブラウザで表示している Path の画面内に存在するコンポーネント一覧がツリーで表示されるのだ。ご丁寧なことに、それが画面上のどの DOM で そのコンポーネントの props には何が設定されているのかまで分かる。

Toggle Component Inspector を有効にすると、Hover 中の DOM が何のコンポーネントで、どのファイルの何行目のコードで生成されているか分かる。

これまで、表示している画面上の UI が、何のコンポーネントで、それはどのファイルでどこにあり、どの様に呼び出されているかは、ソースコードを読まなければ分からなかった。コンポーネントの構造にルールを設け、より直感的に UI 部品の構成を理解できるように考え出されたのが Atomic Design だ。

私がテックリードを務めるプロジェクトでは、コンポーネントのルート要素のクラス名に命名規則を設けることで、コンポーネントが簡単に特定できる工夫をしていた。これは必要なくなる。

これは小さからぬ事件ではないだろうか。フロントエンド開発のベストプラクティスを前進させている。

担当になりたての既存プロジェクトで改修対象のファイル特定のため、レンダリング後の HTML の中からキーワードを拾って Grep 、ファイルを特定するなどという非効率な作業は消滅する。Storybook では Page Component まで定義する必要がなくなるかもしれない。

このツールには他に、有効な画面の Path 一覧、プロジェクト内の画像ファイル一覧、Pinia の 状態表示、コンポーネント間の依存関係グラフ表示などの機能があった。Vite にしても、この DevTools にしても、Vue Framework の開発力と発展速度には恐れ入るばかりだ。

これほどのものなら、現在開発中のプロジェクトで試したくなる。たった 3 行の追加と Package インストールで使用可能になる。

package.json
{
  // 省略
  "devDependencies": {
    // 省略
    "vite-plugin-vue-devtools": "^7.0.25", // 追加
    // 省略
}
vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools' // 追加

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    VueDevTools(), // 追加
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

DevTools の公式によると Vue3 であればどのバージョンでも対応するそうである。Ver 3.3.10 で開発をスタートした私のプロジェクトではすんなり動いた。UI の Hover で、コンポーネントごとにファイル名が表示された時は感動があった。残念ながらその様子をここでお見せすることは出来ないが、コンポーネントグラフであれば問題ないので残しておこう。

不必要な関数やコンポーネントの発見、削除に役立ちそうだ。

型安全芸術への招待 – TypeScript で Object の const assertion と Key, Value の型指定が気に入っている件

d.taniguchi

2023年10月1日 21:31

Post / コメント

  • Java
  • Tips
  • TypeSafetyArt
  • TypeScript

学生の頃、趣味で VBA や COBOL を書いていたのだが、仕事で書くようになったのは Java であった。Java で学んだことは今も血肉として活きているし、代えがたい愛着もある。Java の好きな点はいくつもあるが、とりわけ好きだったのが Enum である。

よくこんなコードを書いた。

Options.java
public interface Options<K extends Serializable, V extends Serializable> {
    K getKey();
    V getValue();

    static <K extends Serializable, ENUM extends Enum<ENUM> & Options<K,?>> ENUM getEnumFromKey(Class<ENUM> clazz, K key) {
        for (ENUM enm : clazz.getEnumConstants())
            if (key.equals(enm.getKey())) return enm;
        throw new IllegalArgumentException();
    }

    static <V extends Serializable, ENUM extends Enum<ENUM> & Options<?,V>> ENUM getEnumFromValue(Class<ENUM> clazz, V value) {
        for (ENUM enm : clazz.getEnumConstants())
            if (value.equals(enm.getValue())) return enm;
        throw new IllegalArgumentException();
    }
}
Enums.java
public class Enums {
    public enum Sex implements Options<Integer, String> {

        FEMALE(0, "女性"),
        MALE(1, "男性");

        private final int key;
        private final String value;
        Sex(int key, String value) { this.key = key; this.value = value; }
        @Override public Integer getKey() { return key; }
        @Override public String getValue() { return value; }
    }
}
User.java
class User {
    @Getter
    @Setter
    private String name;

    @Getter
    private Enums.Sex sex;

    @Getter
    @Setter
    private int age;

    public void setSex(int sex) {
        this.sex = Options.getEnumFromKey(Enums.Sex.class, sex);
    }
}

上記コードは DB から取得した User Entity が、取得時点で int 定数を Enum に変換、カプセル化される様子を再現している。

Java の Enum は Singleton なので、データの一意性や型安全性、不変性を保証するときに強力な効果を発揮する。データ入力時点には意味も型安全も持たなかった int 定数は、意味も型安全も不変性も一意性も有している Enum で Field に保持されるので、それ以降の処理で定数やラベルの誤りなど発生しようがない。

Generics を使用することで、動的型付け言語と同じ型指定の柔軟性を持たせつつ、型の誤りは実装の段階で検出できる。これは動的型付け言語が逆立ちしても実現できない、型安全のためのチェック網だ。これはもう静的型付け言語による、型安全芸術と呼んでよろしかろう。

Java が魅せるオブジェクト指向と静的型付けの世界に耽溺していた私には、 当初 TypeScript に対してよい印象を持つことは難しかった。JavaScript との相互変換を意識するあまり、静的型付け言語としてはあまりに型安全性が脆弱な、”妥協的産物”というイメージがあったからである。例を挙げるなら Union 型だろう。

TypeScript
function getYearOfBirth(age: string|number) {
    let numericAge
    if (typeof age === "string") {
        numericAge = Number(age)
        if (Number.isNaN(numericAge)) {
            throw Error()
        }
    } else {
        numericAge = age
    }
    // 以降省略

この見苦しい関数は何だ。なぜ継承関係がない string 型と number 型の両方が、同じ関数の同じ引数に指定できてしまうのだ?言語仕様がリスコフの置換原則を否定しているではないか。これでは静的型付け以前に、オブジェクト指向言語と言えるのかすら怪しくはあるまいか。

今考えると、言語仕様を妥協したとしても、JavaScript と完全にトランスパイルできるようにしたのは、正しい判断だったと思っている。現実世界では、理想を主張する方が簡単だ。TypeScript を厳格な静的型付け言語に設計し、TypeScript から JavaScript へ変換できても、JavaScript のライブラリは TypeScript で使用不可能だったら、これほど広く使用される言語になったかどうか疑わしい。使われないものに価値はない。 Alt JS のデファクトスタンダード登場が遅れていたかもしれない。Microsoft 社の TypeScript 開発チームのこの英断こそ、プロの仕事と呼ぶに相応しい。

特に面白みもなく書いていた TypeScript が、私の中で輝きを放つきっかけとなったのが Literal 型であった。

TypeScript
type Fruit = "apple" | "orange" | "grape" | "banana"

Literal 型を初めて知ったときは驚愕した。まさか string や boolean といった型にまだ subtype を定義できる余地が残っていたとは思いもよらなかったからだ。Anders Hejlsberg の仕業だろうか。才能豊かな言語設計者の発想には頭が下がる。

Literal 型は Enum と似ている。似ているが 仰々しい Java の Enum より遙かにシンプルな Syntax だ。これはとんでもなくよいアイデアではあるまいか。

TypeScript
type user = {
  name: string,
  age: number
  favoriteFruit: Fruit
  ...
}

これを Key Value 形式のデータ構造である Object 型に応用すれば、上述した Java の Enum と同じような役割を与えることが可能になるではないか。

TypeScript では Object の key を ` <T extends Object> keyof T ` 、value を ` <T extends Object> T[keyof T] `として型定義することができる。

TypeScript
export type Options<K extends number|string, V> = Readonly<Record<K, V>>

export type OptionsKey<T extends Options<any, any>> = keyof T

export type OptionsValue<T extends Options<any, any>> = T[keyof T]


/**
 * Options の value から key を取得する
 *
 * @param options
 * @param value
 * @returns key
 */
export const getOptionsKeyFromValue = <T extends Options<any, any>, K extends keyof T, V extends T[keyof T]>(options: T, value: V): K =>
   (Object.entries(options).find(el => el[1] === value) as [K, V])[0]
TypeScript
export const SEX = {
  0: "女性",
  1: "男性"
} as const satisfies Options<number, string>
TypeScript
export type User = {
  name: string
  sex: OptionsKey<typeof SEX>
  age: number
}

これにより格納される値はただの int 定数であったとしても、コード上は意味が明確に定義された型安全のある別物となった。例えば、テストや Storybook の引数設定などで、Field 値が必要になったとしても、型だけ分かっていれば実値は必要ない。

TypeScript
const user: User = {
  name: "test taro",
  sex: getOptionsKeyFromValue(SEX, "男性"),
  age: 23
}

この関数、さらに Literal 型の効力で選択肢が Suggest される。

何という便利、何という型安全性だろう。これこそ私が目指す型安全芸術だ。ただの JavaScript の Object が、TypeScript の型機能によって、別次元のものとなった。