🤏
unionなオブジェクト型をPickするときはDistributive Conditional TypeでPickする
始めに
Reactでpropsを一部pickした際にunionで定義した情報が消えてしまい条件分岐によって型が変わらなくなってしまっていました。
pickをそのまま使うとunionの情報がなくなる
import { pick } from 'lodash-es'
type Props = {
label: string
required?: boolean
} & (
| {
multiple?: false
value: number | null
onChangeValue: (newValue: number | null) => void
}
| {
multiple: true
value: number[]
onChangeValue: (newValue: number[]) => void
}
)
const Component: FC<Props> = (props) => {
const conditionalProps = pick(props, ['multiple', 'value', 'onChangeValue'])
const handleClear = () => {
// 条件分岐によってonChangeValueの型が変わらなくなってエラーになる
if (conditionalProps.multiple) {
conditionalProps.onChangeValue([])
} else {
conditionalProps.onChangeValue(null)
}
}
}
理由は単純でpickメソッドの返り値であるPickは以下のような実装になっているのでオブジェクト単位でのunionが考慮できずキーごとのunionに変換されてしまいます。
// Pickの内部実装イメージ
type Pick<T, K extends keyof T> = {
[Key in K]: T[Key]
}
// 従って Pick<Props, 'multiple' | 'value' | 'onChangeValue'> の結果は
// 以下のようにキーごとのunionになる
type ConditionalProps = {
multiple: Props['multiple'] // (false | undefined) | true
value: Props['value'] // (number | null) | number[]
onChangeValue: Props['onChangeValue'] // ((newValue: number | null) => void) | ((newValue: number[]) => void)
}
pickせずpropsのまま扱ったり、conditional部分以外のpropだけ分割代入して、conditionalな部分だけrestPropsとして書く運用でもなんとかなりますが、個人的にpickでconditionalな状態を維持することができないか気になったので調査して記事にまとめました。
Distributive Conditional Typeを使ってunion状態を維持してPickする
結論から言うと以下のようにDistributive Conditional Typeを使ってそれぞれにPickが当たるようにします。
unionなオブジェクトに対して、それぞれPickする
type DistributivePick<
T extends object,
K extends keyof T
> = T extends any ? Pick<T, K> : never
// 例のPropsに使用すると以下のようにそれぞれにPickが分配される
type ConditionalProps =
| Pick<{
label: string
required?: boolean
} & {
multiple?: false
value: number | null
onChangeValue: (newValue: number | null) => void
}, 'multiple' | 'value' | 'onChangeValue'>
| Pick<{
label: string
required?: boolean
} & {
multiple: true
value: number[]
onChangeValue: (newValue: number[]) => void
}, 'multiple' | 'value' | 'onChangeValue'>
Distributive Conditional Typeについては以下の記事とかを参考にすると良いと思います。
また余談ですが、この方法をOmitのケースでMUIでもやっているようで、以下にユーティリティとして用意されていました。
終わりに
以上がunionなオブジェクト型をpickする時の方法でした。検証コードをTypeScript Playgroundで書いたので、推論の様子とか気になる方はこちらもご参照いただければと思います。
Discussion