問題
オブジェクト型T
、文字列型K
、型D
が与えられたとき、「K
がT
のプロパティである場合はT[K]
、そうでない場合はD
を返す」型関数 ValueOrDefault<T,K extends string, D>
は、Conditional Typesを使用すれば以下のように簡単に実装できます。
type ValueOrDefault<T, K extends string, D> = K extends keyof T ? T[K] : D;
type T3 = ValueOrDefault<{ a: 1 }, "a", 1>; // 1
type T4 = ValueOrDefault<{ a: 1 }, "a", 2>; // 1
type T5 = ValueOrDefault<{ a: 1 }, "b", 2>; // 2
type T6 = ValueOrDefault<{ a: { d: 1 } }, "a", { e: 2 }>; // { d: 1 }
type T7 = ValueOrDefault<{ a: { d: 1 } }, "b", { e: 2 }>; // { e: 2 }
では、この型関数を、Conditional Typesおよびany
を使用せずに実装してください。
Typescriptの想定バージョン: v4.0.2
解答
おそらく型パズルとしては中級以上ではないかと思います。
当初、解答2を思いついたのですが、投稿後にもっと簡単なものがあるのに気づきました。
というか、@kazatsuyuさんのコメントに書かれたのが、一番TypescriptのMapped typeの考え方に近いと思います。
解答1
// T[K] if K in keyof T
// never if K not in keyof T
type ValueOfWithNever<T, K extends string> =
T[keyof T & K];
type MapNever<T> = { [P in keyof T]: never };
// never if K in keyof T
// D if K not in keyof T
type DefaultOrNever<T, K extends string, D> =
(MapNever<T> & {[P in K]: D})[K];
// T[K] if K in keyof T
// DefaultValue if K not in keyof T
type ValueOrDefault<T, K extends string, D> =
| DefaultOrNever<T, K, D>
| ValueOfWithNever<T, K>;
type T3 = ValueOrDefault<{ a: 1 }, "a", 1>; // 1
type T4 = ValueOrDefault<{ a: 1 }, "a", 2>; // 1
type T5 = ValueOrDefault<{ a: 1 }, "b", 2>; // 2
type T6 = ValueOrDefault<{ a: { d: 1 } }, "a", { e: 2 }>; // { d: 1 }
type T7 = ValueOrDefault<{ a: { d: 1 } }, "b", { e: 2 }>; // { e: 2 }
解答2
// T[K]
type ValueOf<T, K extends keyof T> = T[K];
type ValueOfByObject<
T extends { [P in keyof KeyObject]: {} },
KeyObject
> = ValueOf<T, keyof KeyObject>;
type PassObjectToValueOfByObject<
A extends { x: {} },
B extends { x: {} }
> = ValueOfByObject<A["x"], B["x"]>;
// keyof T の制約のかかっていないKで、T[K]相当の事を行う
type ValueOfWithoutKeyofConstraint<T, K extends string> =
PassObjectToValueOfByObject<{ x: T }, { x: { [P in K]: {} } }>;
type T1 = ValueOfWithoutKeyofConstraint<{ a: 1 }, "a">; // 1
type T2 = ValueOfWithoutKeyofConstraint<{ a: 1 }, "b">; // unknown
// {[K]: T[K]}
type LimitKey<T, K extends keyof T> = { [P in keyof T]: { [K in P]: T[K] } }[K];
type LimitKeyByObject<
T extends { [P in keyof KeyObject]: {} },
KeyObject
> = LimitKey<T, keyof KeyObject>;
type PassObjectToLimitKeyByObject<
A extends { x: {} },
B extends { x: {} }
> = LimitKeyByObject<A["x"], B["x"]>;
// {[K]: T[K]} if K in keyof T
// unknown if K not in keyof T
type LimitKeyWithoutKeyofConstraint<T, K extends string> =
PassObjectToLimitKeyByObject<{ x: T }, { x: { [P in K]: {} } }>;
type MapNever<T> = { [P in keyof T]: never };
// T[K] if K in keyof T
// never if K not in keyof T
type ValueOfWithNever<T, K extends string> =
T[keyof LimitKeyWithoutKeyofConstraint<T, K>];
// never if K in keyof T
// D if K not in keyof T
type DefaultOrNever<T, K extends string, D> =
ValueOfWithoutKeyofConstraint<MapNever<T>, K> & D;
// T[K] if K in keyof T
// DefaultValue if K not in keyof T
type ValueOrDefault<T, K extends string, D> =
| DefaultOrNever<T, K, D>
| ValueOfWithNever<T, K>;
type T3 = ValueOrDefault<{ a: 1 }, "a", 1>; // 1
type T4 = ValueOrDefault<{ a: 1 }, "a", 2>; // 1
type T5 = ValueOrDefault<{ a: 1 }, "b", 2>; // 2
type T6 = ValueOrDefault<{ a: { d: 1 } }, "a", { e: 2 }>; // { d: 1 }
type T7 = ValueOrDefault<{ a: { d: 1 } }, "b", { e: 2 }>; // { e: 2 }
解説?
もともと、この型関数の型制約を外すテクニックに関する記事が書きたかったのですが、牛刀割鶏でしたね。
解答の中のValueOf
~ValueOfWithoutKeyofConstraint
の行のようなテクニックを使用すると、なぜか型関数の型制約が外せます。ただ、なぜこれで型制約が外れるのかはよくわからないです(コードを書いていたら、たまたま発見した)。なんだかTypescriptのバグっぽい気もします。どなたか分かる方、コメントをよろしくお願いいたします。
これが何の役に立つかはわからないです。一般的には、Conditional Typeを使うと型推論がうまくいかなくなることが多いですが、このテクニックを使って何か型推論がうまくいくようになった印象はないです。