724 文字
4 分

Reactのpropsをunion typeにする方法について

Union types of props#

複雑なサービスを作っていると、Union types を 1 つのメソッドで処理したくなることがあるかもしれない。しかし安直に書いてしまうと上手くいかない。このような型定義にライブラリで出会ってしまうと最悪な気持ちになる。

export type Props =
| {
text: Item;
}
| {
multilineText: Item;
}
| {
select: Item;
};
export const SomeComponent = (props: Props) => {
return (
<div>
{props?.text ? (
<div>{props?.text.value}</div>
) : props?.multilineText ? (
<div>{props?.multilineText.value}</div>
) : props?.select ? (
<div>{props?.select.value}</div>
) : null}
</div>
);
};

Kind pattern#

各型に共通のプロパティを持たせ、それぞれの値を固有の Literal type などにするパターン。

このプロパティを比較することで、どの型を利用するかの識別が可能になる。

export type Props =
| {
kind: "text";
text: Item;
}
| {
kind: "multilineText";
multilineText: Item;
}
| {
kind: "select";
select: Item;
};
const SomeComponent = (props: Props) => {
return (
<div>
{props?.kind === "text" ? (
<div>{props?.text.value}</div>
) : props?.kind === "multilineText" ? (
<div>{props?.multilineText.value}</div>
) : props?.kind === "select" ? (
<div>{props?.select.value}</div>
) : null}
</div>
);
};

Optional never Trick#

次のように、擬似的に全ての型へ同様のプロパティを持たせることで対応できる。

このパターンの場合、レスポンスなどで受け取ったオブジェクトを編集しなくていい場合があるというメリットと、プロパティを列挙しなければならないというデメリットがある。

export type Props =
| {
text: Item;
multilineText?: never;
select?: never;
}
| {
text?: never;
multilineText: Item;
select?: never;
}
| {
text?: never;
multilineText?: never;
select: Item;
}
| {
text?: never;
multilineText?: never;
select?: never;
};
const SomeComponent = (props: Props) => {
return (
<div>
{props?.text ? (
<div>{props?.text.value}</div>
) : props?.multilineText ? (
<div>{props?.multilineText.value}</div>
) : props?.select ? (
<div>{props?.select.value}</div>
) : null}
</div>
);
};

次のような Utility type を生やしておけば、デメリットを解消できる。

type XOR<
T extends Record<string, unknown>,
U extends string = T extends T ? keyof T : never
> = T extends T
? {
[k in keyof T]: T[k];
} & {
[k in Exclude<U, keyof T>]?: never;
}
: never;
export type Props = XOR<
| {
text: Item;
}
| {
multilineText: Item;
}
| {
select: Item;
}
>;
export const SomeComponent = (props: Props) => {
return (
<div>
{props?.text ? (
<div>{props?.text.value}</div>
) : props?.multilineText ? (
<div>{props?.multilineText.value}</div>
) : props?.select ? (
<div>{props?.select.value}</div>
) : null}
</div>
);
};

Utility types of TypeScript#

https://www.typescriptlang.org/docs/handbook/utility-types.html

TypeScript には便利なユーティリティタイプがいくつか同梱されている。

個人的に最低限知っておいた方がいいと思うものを列挙する。

  • Required:Object の全てプロパティを必須にできる
  • Partial:Object の全てのプロパティをオプショナルにできる
    const Page = (props: PageProps) => {
    return ...
    }
    type Params = Partial<PageProps>
    const PageContainer = ( ) => {
    const params = useParams<Params>()
    return params?.index === undefined ? null : <Page {...params} />
    }
    • Parameters:関数の引数を取得する
    • ReturnType:関数の戻り値を取得する
    const requestGetSomething = (args: Parameters<typeof endpoints.getSomething>) => {
    return fetcher(endpoints.getSomething(args), {...})
    }
    • Pick<Object, Keys>:オブジェクトから特定のプロパティのみを取り出す
    • Omit<Object, Keys>:オブジェクトから特定のプロパティを取り除く
    interface TodoProps {
    onEdit?: () => void;
    }
    interface ReadonlyTodoProps extends Omit<TodoProps, "onEdit"> {}
Reactのpropsをunion typeにする方法について
https://blog.ohirunewani.com/posts/react-props-union-type/
作者
hrdtbs
公開日
2022-11-03
ライセンス
CC BY-NC-SA 4.0