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?

TypeScript 7.1 Beta で厳しくなった Distributive Conditional Types の制約チェックを理解する

0
Posted at

はじめに

2026年10月20日に TypeScript 7.1 Beta が公開されました。

公式ブログの「Notable Behavioral Changes」の中で、Distributive Conditional Types の制約チェック強化(Stronger Checks When Using Distributive Type Parameters)が紹介されています。

この記事では、余計なプロパティを弾く Exact 型を例に、7.1 で何がエラーになり、どう直せばよいのかをまとめます。
検証には typescript@7.0.2 と typescript@7.1.0-beta を使いました。

余計なプロパティを弾く Exact 型

TypeScript は構造的部分型を採用しています。
そのため、必要なプロパティさえ持っていれば、余計なプロパティが含まれていても代入できます。

この「必要なプロパティを持つこと」と「余計なプロパティを持たないこと」を両方要求するのが、次の Exact 型です。

type Exact<P, I extends P> = P extends object
  ? { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, keyof P>]: never }
  : P;

P は期待する型、I は実際に渡された値の型です。
I extends P で必要なプロパティがあることを保証します。
さらに Exclude<keyof I, keyof P> で P にないキーを never にし、余計なプロパティを弾きます。

各プロパティに再帰的に適用するため、ネストしたオブジェクトの余計なプロパティも検知できます。

例えば、プロパティにユニオン型を持つ次のようなコードを考えてみます。

type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; size: number };
type Item = { name: string; shape: Circle | Square };

declare function f<I extends Item>(input: I & Exact<Item, I>): void;

f({ name: "x", shape: { kind: "circle", radius: 1 } });            // OK
f({ name: "x", shape: { kind: "circle", radius: 1 }, extra: 1 });  // エラー
f({ name: "x", shape: { kind: "circle", radius: 1, extra: 1 } });  // エラー

TypeScript 7.0 までは、コメントのとおりに余計なプロパティが弾かれ、型の定義自体にもエラーは出ませんでした。

TypeScript 7.1 で出るエラー

ところが、まったく同じコードを TypeScript 7.1 Beta でチェックすると、Exact の定義の Exact<P[K], I[K]> の部分で2つのエラーが出ます。

error TS2344: Type 'I[K]' does not satisfy the constraint 'P[K]'.
  Type 'I' is not assignable to type 'P'.
    'I' is only assignable to the non-distributed 'P', but 'P' has been distributed here.
error TS2536: Type 'K' cannot be used to index type 'I'.

呼び出し側の f(...) の3行の判定結果は 7.0 と変わりません。
エラーになるのは、Exact 型の定義だけです。

実例として、protobuf のコード生成ツール「ts-proto」が出力する Exact 型でも、7.1 で同じエラーが発生しています。

分配された型パラメータと制約のズレ

エラーの原因を追うために、型が内部でどう評価されているかを確認します。

先ほどのコードの1行目、f({ name: "x", shape: { kind: "circle", radius: 1 } }) を呼んだときを考えます。
このとき、shape プロパティに対しては Exact<Circle | Square, { kind: "circle"; radius: 1 }> が評価されます。

Exact 定義の P extends object ? ... : ... は Distributive Conditional Types なので、ユニオン型 Circle | Square が次のように要素ごとに分配されます。

評価中の P I extends P が成り立つか
Circle 成り立つ
Square 成り立たない(I は Circle の形状)

制約の I extends P が保証しているのは、分配前の P(Circle | Square)に対してだけです。
分配された後の個別の P(Circle や Square)に対して I が代入できるかは保証していません。

エラーメッセージの non-distributed と distributed は、この分配前と分配後を指しています。

一方で、再帰呼び出しの Exact<P[K], I[K]> では、Exact 自身の制約により I[K] が P[K] に代入可能であることが求められます。
TypeScript がこれを確かめるには I が分配後の P(例えば Square)を満たす必要がありますが、その保証はありません。

7.1 のエラーは、このズレを指摘しています。

  • TS2344:I が分配後の P(例えば Square)を満たすとは限らない
  • TS2536:分配後の P が持つキー K(例えば Square の size)を、I が持っているとは限らない

この変更のきっかけとなった Issue と修正 PR はこちらです。

なぜ 7.0 までは動いていたのか

型定義にこのような不整合があるなら、なぜ 7.0 までは正常に動いていたのでしょうか。

実は、型の計算結果自体は 7.0 と 7.1 で変わっていません。
Exact<Circle | Square, { kind: "circle"; radius: 1 }> は、どちらのバージョンでも次の型になります。

{ kind: "circle"; radius: number } | { kind: "square"; size: number; radius: never }

後半の Square 側の分岐は、Circle の値を Square に無理やり当てはめた無意味な型です。
ただし、この型は kind: "square" を要求します。
渡された値(kind: "circle")がこの分岐にマッチすることはありません。

そのため、呼び出し側での判定結果は正しく保たれていました。

つまり、7.1 のエラーは「既存の挙動が壊れていた」から出たのではありません。
「型の定義の中で、分配前の制約を分配後にも成り立っている前提で使うのはおかしい」と厳しくチェックするようになったためです。

直し方

このエラーを直すには2通りのアプローチがあります。

1. 分配後に制約を再チェックする

分配した直後に I extends P をもう一度評価します。
これで、分岐の中では I が分配後の P を満たすことが保証されます。

type Exact<P, I extends P> = P extends object
  ? I extends P
    ? { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, keyof P>]: never }
    : never
  : P;

7.0 と 7.1 の両方でエラーなく通り、呼び出し側の判定結果も変わりません。
マッチしない分岐(先ほどの例における Square 側)が never になるため、無意味なユニオンの枝が残らなくなるメリットもあります。

2. 制約を外し、キーの有無を型の中でチェックする

もう一つの方法は、型パラメータの制約 I extends P を外すアプローチです。
キーが存在するかどうかを、型の中で個別にチェックします。

type Exact<P, I> = P extends object
  ? { [K in keyof P]: K extends keyof I ? Exact<P[K], I[K]> : P[K] }
    & { [K in Exclude<keyof I, keyof P>]: never }
  : P;

こちらも 7.0 と 7.1 の両方でエラーなく通り、呼び出し側の判定結果も変わりません。
ts-proto の修正(v2.18.1)でもこの方針が採られています。

おわりに

TypeScript 7.1 では、この変更に加えて API も追加されました。

7.0 では外部向け API が提供されていなかったため、typescript-eslint などのツールは 7 系に対応できず、6 系と併用する必要がありました。
現在 typescript-eslint で 7.1 への対応が進められており、正式リリース後はツール類も含めて 7 系に揃えられるようになりそうです。

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?