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?

More than 3 years have passed since last update.

Typescript 型パズル: Conditional Typesを使わずにプロパティかどうかで条件分岐

Last updated at Posted at 2020-10-22

問題

オブジェクト型T、文字列型K、型Dが与えられたとき、「KTのプロパティである場合は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 }

解説?

もともと、この型関数の型制約を外すテクニックに関する記事が書きたかったのですが、牛刀割鶏でしたね。

解答の中のValueOfValueOfWithoutKeyofConstraintの行のようなテクニックを使用すると、なぜか型関数の型制約が外せます。ただ、なぜこれで型制約が外れるのかはよくわからないです(コードを書いていたら、たまたま発見した)。なんだかTypescriptのバグっぽい気もします。どなたか分かる方、コメントをよろしくお願いいたします。

これが何の役に立つかはわからないです。一般的には、Conditional Typeを使うと型推論がうまくいかなくなることが多いですが、このテクニックを使って何か型推論がうまくいくようになった印象はないです。

0
0
2

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?