1
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?

TypeSpecでRecord<unkown>を使うとTypeScriptでRecord<string, never>と評価されてしまう問題

Last updated at Posted at 2025-12-05

TypeSpecアドカレ記事です!

結論

TypeSpecのEmitterでOpenAPI 3.1.0を指定するとRecord<unknown>unevaluatedPropertiesとして評価され、TypeScriptへの変換時にRecord<string, never>として解釈されてしまうので3.0.0を利用する。

3.0.0に戻すとadditionalPropertiesとして評価され、TypeScriptでは Record<string,unknown>として解釈される。

前提

利用ライブラリ

  • TypeSpec
  • OpenAPI 3.0.0/3.1.0
  • OpenAPI TypeScript

背景

TypeSpecを利用し、以下のような流れで型・IFの自動生成を行おうとした。

TypeSpec->OpenAPI

OpenAPI->TypeScript
OpenAPI->Python

APIのレスポンスには以下のような動的キーのObject配列が含まれていたため、連想配列のスキーマ定義が必要となった。

const response = {
    "KEY-001": {
        name: "Sample001",
        key: "49vksdx9a3kf",
        //以降いくつかのプロパティ
    },
    "KEY-002": {
        name: "Sample002",
        key: "95dgf9gdrd",
        //...
    },
    //...
}

連想配列のキーは動的に生成されるため、TypeScriptでは以下のような型が期待される。

type ResponseItem = {
    name: string,
    key: string,
    //...
}

type Response = Record<string, ResponseItem>

ケースの説明

上記のような型を生成するにはTypeSpecで以下のように定義する必要がある。

//model.tsp

model ResponseItem {
    name: string;
    key: string;
    //...
}

model Response = Record<ResponseItem>

しかし、APIの仕様上レスポンスに含まれるプロパティが動的に変動する可能性がある場合があったため以下のように定義を変更した。

model ResponseItem {
    name: string;
    key: string;
    ...Record<unknown>;
}

決まったkey/valueで返却されるプロパティと動的プロパティ両方を持ち、namekeyは推論できるようにしつつ柔軟に他のプロパティも受け入れることが目的だった。

期待するTypeScriptの生成型は以下のとおり。

export interface components {
    schemas: {
        ResponseItem: {
            name: string;
            key: string;
        } & {
            [key: string]: unknown;
        }
    }
}

起きた問題

TypeSpecのモデル定義に Record<unknown>が含まれている場合、OpenAPI TypeScriptで生成される型が Record<string, never> になってしまう。

問題の原因

OpenAPI TypeScriptの仕様を調べる

OpenAPI TypeScriptでは unevaluatedProperties: {}のオプションを持つschemasを Record<string, never>として変換する仕様となっている。
additionalProperties: {}を持っていれば上記で期待した通りの型が生成される。

TypeSpecからOpenAPIへの変換時の挙動を調べる

以下の記事ではOpenAPI3.1.0ではRecord<unknown>の評価時、additionalPropertiesの代わりにunevaluatedPropertiesを利用している旨が記載されている。

Note: unevaluatedProperties is used instead of additionalProperties when emitting Open API 3.1 specs.

しかし、additionalPropertiesunevaluatedPropertiesは別モノなのでOpenAPI TypeScriptでは明確に変換時の振る舞いが変わってしまう。

additionalPropertiesunevaluatedPropertiesの違い

additionalProperties→OpenAPI3.0.0から存在する属性。falseを指定するとallOf等で統合する際、子スキーマ側で拡張したプロパティを許可されていないプロパティとみなしてしまうことがある。

unevaluatedProperties→OpenAPI3.1.0から登場した属性。対象の階層で検証が行われたすべてのプロパティを「既知」とみなし、検証済みの全プロパティが既知とみなされる。
allOfで起きる上記の問題を解決している。
ただし計算コストはやや高め。

対策

TypeSpecのemitterを3.0.0に戻す

3.0.0のemitterであれば Record<unknown>additionalPropertiesとして評価してくれるので、現在はこれが手っ取り早い。
3.2.0以降でunevaluatedPropertiesの扱いがどうなるか、OpenAPI TypeScript側での解釈がどう変わるか引き続き注目する。

1
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
1
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?