意味のわからないタイトルになってしまいましたがReduxなどでは意外とやりたくなるパターンかと思います。
type FooAction = {
type: 'Foo',
payload: string
};
type BarAction = {
type: 'Bar',
payload: number
};
type Action = FooAction | BarAction;
みたいなのがあったときに
type SomeAction = FilterActionByType<Action, 'Foo'>; // FooAction
のようなことがしたい。
結論
以下のようにします。
type FilterUnionByProperty<
Union,
Property extends string | number | symbol,
Condition
> = Union extends Record<Property, Condition> ? Union : never;
冒頭のFilterActionByType
は
type FilterActionByType<Action, Type> =
FilterUnionByProperty<Action, 'type', Type>
のようにできます。
仕組み
最初は以下のようなコードを書きました。
type FilterActionByType<Action extends { type: any }, Type> =
Action['type'] extends Type ? Action : never;
しかしながら、これではなぜか皆never
になってしまいます。
調べたところ、この方法ではAction['type']
がdistributive conditional typesとして扱われないためだとのこと(参考)。
distributive conditional typesの説明を見ると
Conditional types in which the checked type is a naked type parameter are called distributive conditional types.
となっています。Action['type']
は裸(naked)ではない型引数のため、distributive conditional typesとならないのです。
先ほどのStack Overflowの回答を見ると、以下のようなコードも載っていました。
export type FilterActionByType<
A extends AnyAction,
ActionType extends string
> = A extends any ? A['type'] extends ActionType ? A : never : never;
type ActionUnion = Action<'count/get'> | Action<'count/set'>;
type CountGetAction = FilterActionByType<ActionUnion, 'count/get'>;
// type CountGetAction = Action<"count/get">
A extends any ? ~ : never
によりラップすることで問題を回避していますね。なんてトリッキーな...。
いやはや、TypeScriptは奥が深い。そして設計者に改めて敬意を示したくなりますね!