TypeScriptの合併型・交差型の使い方と注意点

TypeScriptの型定義に使用される合併型(Union Types)と交差型(Intersection Types)の使い方と注意点についてまとめました。

型定義は複雑になる場合がありますが、合併型と交差型をシンプルに理解しておくことで、間違いを減らす助けになると思います。

また、しばしば見かける「TypeScriptの交差型は上書きに使用する」という表現は誤解を招く恐れがあると感じましたので、実例を交えて注意点を記載します。

1章は基本ですので、実用レベルの話だけ気になる人は2章から、交差型の注意点については3章から読んでください。

合併型・交差型とは

TypeScriptの合併型・交差型は、2つ(以上)の型を組み合わせて新たな型定義を作るための機能です。

例えば、stringnumberの合併型は、文字列「または」数値を値として受け付けます。

{"code":"type UserId = string | number;\n\/\/ OK\nconst user1_id: UserId = 1;\n\/\/ OK\nconst user2_id: UserId = 'hfe2-3241-klaw-fewt';\n\/\/ NG\nconst user3_id: UserId = false;","language":"typescript","id":2,"filename":""}

一方、交差型は「かつ」を表す型であり、指定された型の要件を全て満たした値しか受け付けません。そのため、互いに矛盾する型を指定すると値を代入することができません。

{"code":"\/\/ string \u304b\u3064 number\u306f\u3001\u578b\u5b9a\u7fa9\u3067\u304d\u308b\u304c\u73fe\u5b9f\u306e\u5024\u306b\u306f\u5b58\u5728\u3057\u306a\u3044\ntype Something = string && number;\n\/\/ \u5168\u3066NG\nconst a: Something = 1;\nconst b: Something = 'Mike';\nconst c: Something = false;","language":"typescript","id":2,"filename":""}
矛盾した型同士の交差型は、何も代入できないnever型で評価される

string, number, booleanなどのプリミティブ型同士の合併型・交差型の理解は易しすぎますので、次は複数のプロパティを持つ型同士の合併型・交差型を考えます。

集合でシンプルに理解する

実用上は、複数のプロパティを持つ型同士で合併型や交差型を作ることになります。例として、食品を表すGrocery型と、雑貨などを表すGoods型を考えてみます。

{"code":"type Grocery = {\n  id: number;\n  name: string;\n  best_before_date: Date;\n  country_of_origin: string;\n};\ntype Goods = {\n  id: number;\n  name: string;\n  maker: string;\n  category: string;\n};","language":"typescript","id":2,"filename":""}
Grocery型とGoods型のプロパティ。
※TypeScriptでは型の継承や拡張ができるため分かりやすさのためクラス図を利用しましたが、厳密に正しい表現では無いことをご容赦ください

2つ以上の型を用いて合併型・交差型を作ったとき、合併型は和集合・交差型は積集合を表します。

合併型Grocery | Goodsと交差型Grocery & Goodsは以下のベン図で表されます。

Grocery型とGoods型の合併型・併合型

合併型はベン図の白い囲みのエリア、交差型はベン図の緑色のエリアに属する型となります。具体的には、

  • 合併型が受け付けるオブジェクト
    → Grocery型が持つプロパティを全て持っている、または、Goods型が持つプロパティを全て持っているオブジェクト
  • 交差型が受け付けるオブジェクト
    → Grocery型とGoods型が持つ6種類のプロパティを持っているオブジェクト
コマリブル
コマリブル

シンプルに理解しておくと、実用で使う型定義でも困らないらしいぞ。

値を代入して確かめてみよう

前節でつくった合併型・交差型に値を代入して、どんな値を許容するか確かめてみます。

合併型(Union Type)が受け付ける値

まず、合併型では、2つの型にあるプロパティが全て使用できます

用途を想像して当てはめをしてみます。ネイティブなGrocery型にはmakerプロパティがありません。しかし、例えばパスタやレトルト食品は、特定のメーカーで大量生産されている製品なので、管理にmakerプロパティを使用したくなるかもしれません。

パスタのような大量生産品には、makerプロパティが欲しいかもしれない
{"code":"type Grocery = { ... };\ntype Goods = { ... };\n\/\/ Grocery\u3068Goods\u306e\u5408\u4f75\u578b\ntype Item = Grocery | Goods;\nconst pasta:Item = {\n  id: 100,\n  name: 'No.11 Spaghettini',\n  country_of_origin: 'Italy',\n  best_before_date: new Date(2028, 12, 31),\n  \/\/ maker\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u8ffd\u52a0\u3067\u304d\u308b\n  maker: 'DE CECCO'\n}","language":"typescript","id":2,"filename":""}

同様に、合併型Itemsは、GoodsのプロパティにGroceryのプロパティを追加したオブジェクトも表現することができます。

ただし、Grocery型の要件もGood型の要件も満たしていないオブジェクトを代入はできません。

例えば、Grocery型に無いmakerプロパティは追加したけど、もともと持っているbest_before_date(賞味期限)プロパティを削除してしまってはいけません。

{"code":"type Grocery = { ... };\ntype Goods = { ... };\ntype Item = Grocery | Goods;\n\/\/ NG\nconst someFood:Item = {\n  id: 200,\n  name: 'foo',\n  country_of_origin: 'Japan',\n  maker: 'FOOBARBAZ Corporation'\n}","language":"typescript","id":2,"filename":""}

エラーの出方は開発環境によりますが、私の環境では以下のようにエラーになりました。

someFoodオブジェクトにcategoryプロパティを足すとGoods型の要件を満たすため、Property ‘category’ is missing in type …と、categoryプロパティが欠けている旨を指摘されます。

合併型に指定されたいずれの型の要件も満たさないオブジェクトは代入できない
someFoodはGroceryとGoodsの合併型で表現できない
合併型 Union Type

合併された型の全てのプロパティを使用できる。ただし、いずれの型の要件にも当てはまらない値は代入できない。

交差型(Intersection Type)が受け付ける値

次に、交差型に値を代入してみます。交差型はわかりやすく、両方のプロパティを 持つような値を受け付けます。

{"code":"type Grocery = { ... };\ntype Goods = { ... };\n\/\/ Grocery\u3068Goods\u306e\u4ea4\u5dee\u578b\ntype IntersectionItem = Grocery & Goods;\n\/\/ Grocery \u3068Goods\u306e\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u5168\u90e8\u6307\u5b9a\u3059\u308b\nconst pasta:IntersectionItem = {\n  id: 100,\n  name: 'No.11 Spaghettini',\n  country_of_origin: 'Italy',\n  best_before_date: new Date(2028, 12, 31),\n  maker: 'DE CECCO',\n  category: 'staple food'\n}","language":"typescript","id":2,"filename":""}

1つでもプロパティが足りないとTypeScriptはエラーを返します。例えば、makerプロパティを消してみたところ、予想通りに指摘されました。

{"code":"type IntersectionItem = Grocery && Goods;\n\nconst someThing:IntersectionItem = {\n  id: 100,\n  name: 'No.11 Spaghettini',\n  country_of_origin: 'Italy',\n  best_before_date: new Date(2028, 12, 31),\n  \/\/ maker\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u6d88\u3057\u305f\n  category: 'staple food'\n}","language":"typescript","id":2,"filename":""}
makerプロパティが無いと指摘される

ここでは、someThingはGrocery型のプロパティは全て持っていますがGoods型に必要なmakerプロパティが無いオブジェクトであり、交差型 Grocery & Goodsでは表現できません。

someThingは、合併型では表現できるが交差型では表現できないオブジェクト

ベン図からわかるように、交差型を使うと必ず型定義は狭くなり、代入できる値は限定的になります。

交差型を実用する時に勘違いしないためにも、このことを意識しておくと良いです。

交差型 Intersection Type

指定された型が持つ全てのプロパティを持つ値のみ代入できる。交差型を使うと、型定義が必ず “狭く” なる

次章から、実用で登場しそうな例で合併型と交差型の使用方法と、特に交差型の注意点についてまとめていきます。

合併型の利用法

合併型はTypeScriptの基本機能なので、登場する場面を網羅することはできませんが、よくありそうなコードパターンをいくつか考えてみます。

数種類の型のいずれかを受けたい関数

1章で、合併型の機能の説明のために、合併された他の型のプロパティを利用する例を挙げました。

しかし、実用上はそのようなケースより、「数種類のいずれかの型を引数に取る関数を作りたい」ケースの方が多いように感じます。

ReactのReducer関数

ReactのReducer関数は、典型的な合併型の実用です。Reduxを使用するしないに関わらず、Reducer関数には以下の特徴があります。

  • Reducer関数は現在のStateとActionを受け取り、更新後のStateを返す純関数である
  • Actionには複数の種類があるが、Actionの種類ごとに型は決まっている

Todoリストを更新する典型的なReducer関数の疑似コードとして一例を挙げます。

{"code":"function todosReducer(state: TodosState, action: Action): TodosState {\n  switch (action.type) {\n    case 'ADD_TODO':\n      return { ... }\n    case 'REMOVE_TODO':\n      return { ... }\n    case 'EDIT_TODO':\n      return { ... }\n    default:\n      return state;\n  }\n} ","language":"typescript","id":2,"filename":""}

todoリストを更新するアクションを比較します。

todoリストを追加するADD_TODOを発火するには、Reducer関数に追加したい新規todoの内容を渡す必要がありますが、todoリストを削除するREMOVE_TODOを発火する際は、削除したい既存todoのidだけあれば十分です。

よって、少なくともtodosReducer関数の引数actionには数種類の型を許容する必要があり、合併型で定義するのが良さそうだと気づきます。

ADD_TODOアクションで渡すデータ型とREMOVE_TODOアクションで渡すデータ型の比較
※便宜的にクラス図で表現しています

上記のtodosReducer関数の引数Actionは以下のように合併型で定義しておけば機能します。

{"code":"export type Todo = {\n  id: string;\n  title: string;\n  description: string;\n};\ntype AddTodoAction = {\n    type: 'ADD_TODO';\n    payload: Todo;\n};\ntype RemoveTodoAction = {\n    type: 'REMOVE_TODO';\n    id: string;\n};\ntype EditTodoAction = {\n    type: 'EDIT_TODO';\n    updates: Todo;\n};\n\/\/ \u5404Action\u306etype\u3092\u5408\u4f75\u3057\u3066\u5b9a\u7fa9\u3059\u308b\ntype Action = AddTodoActionToSync | RemoveTodoAction | EditTodoAction;","language":"typescript","id":2,"filename":""}

なお上記のReducer関数は、サンプルとして作成したミニTodoアプリに使用しています。説明の簡単のために実装を省略しているので、気になる方はGitHubで見てみて下さい。

合併型と型ガード(Type Guard)

合併型は数種類の型を許容しますが、合併型の弊害も存在します。複数の型を想定した関数であるがゆえに、特定の型に行うことのできない操作を記述する恐れがあるからです。

TypeScriptはそのような記述を自動的にチェックしエラーを起こします。

合併型を引数に取る関数では、ある型に行うことのできない操作を実行する恐れが無いことを明示するために、型ガード(Type Guard)という考え方で分岐を記述することが多いです。

型ガード(Type Guard)

例を示します。selector関数は、商品の配列から100円以上の商品のみを返すことを意図した関数です。

{"code":"type Item = {\n  id: number;\n  price: number;\n};\n\ntype Items = Array;\n\nfunction selector(data: Items){\n  return data.filter((item) => (\n    item.price > 100\n  ));\n}","language":"typescript","id":2,"filename":""}

selector関数の引数の型Itemsは、Item型またはundefindが入った配列として、合併型を使用して定義されています。

例えば、データがAPI経由で取得されるなどして、(selectorで選別する段階では)不正なデータが混入することを防止できない場合です。

{"code":"\/\/ \u60f3\u5b9a\u3055\u308c\u308b\u30c7\u30fc\u30bf\nconst purchases:Items = [\n    { id: 1, price: 50 },\n    undefined,\n    { id: 2, price: 150},\n    undefined,\n    { id: 100, price: 102 }\n];","language":"typescript","id":2,"filename":""}

この時、undefindをケアした型ガードを設定していない状態では、TypeScriptはselector関数をチェックしてエラーを返します。

undefindな値のpriceプロパティを参照する恐れがある

TypeScriptに、「undefindな値に対して、存在しないpriceプロパティを参照する恐れはありませんよ」と教えるためには、以下のように型ガードを加えます。

{"code":"type Item = {\n  id: number;\n  price: number;\n};\n\ntype Items = Array;\n\n\/\/ \u578b\u30ac\u30fc\u30c9\u3092\u52a0\u3048\u305f\nfunction selector(data: Items){\n  return data.filter((item) => (\n    typeof item !== 'undefined' && item.price > 100\n  ));\n}","language":"typescript","id":2,"filename":""}

不正データの処理に見られるような、消極的な理由に基づいて合併型を利用した際に必要になる型ガードは、プログラミング言語全般で実装される例外処理と同じ意味合いです。

次節では、積極的な理由で合併型を使用した際の型ガードについても言及します。

Reactのラッパーコンポーネント

例えば、以下のラッパーコンポーネントMessageBoxは、通常時はユーザーに補足情報を与えるインフォボックスとして機能し、エラーが発生した時のみエラーメッセージを表示します。

{"code":"type InfoBoxProps = {\n  mode: 'info';\n  children: ReactNode;\n}\ntype WarningBoxProps = {\n  mode: 'warning';\n  severity: 1 | 2 | 3;\n  children: ReactNode;\n}\ntype MessageBoxProps = InfoBoxProps | WarningBoxProps;\nexport default function MessageBox(props: MessageBoxProps){\n  const { children, mode } = props;\n  \/\/ Type Guard \u578b\u30ac\u30fc\u30c9\n  if (mode === 'info'){\n    return(\n      <div className = 'message-box'>\n          <p>Info<\/p>\n          <div className = 'info-message'>{children}<\/div>\n      <\/div>\n    );\n  }\n \n  const { severity } = props;\n  return(\n    <div className = 'message-box'>\n       <p>Warning!<\/p>\n       <div className = 'warning-message-${severity}'>{children}<\/div>\n    <\/div>\n  );\n}","language":"tsx","id":17,"filename":""}
  • 引数MessageBoxPropsは、InfoBoxPropsWarningBoxPropsの合併型で定義
  • インフォモードと警告モードで、返すJSXが違う
  • 警告モードの時のみ、severity(重大度)ごとにスタイリングを行う

ポイントですが、severityプロパティはInfoBoxPropsには存在しないため、以下のようなコードはTypeScriptに指摘されエラーになります。

{"code":"type MessageBoxProps = InfoBoxProps | WarningBoxProps;\nexport default function MessageBox(props: MessageBoxProps){\n  \/\/ severity\u306fwarning\u30e2\u30fc\u30c9\u306eWarningBoxProps\u306b\u3057\u304b\u5b58\u5728\u3057\u306a\u3044\n  const { children, mode, severity } = props;\n  \/\/ ...\n}","language":"typescript","id":2,"filename":""}

そのため、最初のコードのように、型ガードを設定するとエラーが消えて使用できるようになります。

フルバージョンは最初に掲載しましたので、概念が分かりやすいように短くした疑似コードを記載します。

{"code":"type InfoBoxProps = { mode: ...;  children: ...; }\ntype WarningBoxProps = { mode: ...; severity: ...; children: ...; }\ntype MessageBoxProps = InfoBoxProps | WarningBoxProps;\nexport default function MessageBox(props: MessageBoxProps){\n  \/\/ \u4e21\u65b9\u306b\u5171\u901a\u306e children, mode props\u306e\u307f\u53d7\u3051\u53d6\u308b\n  const { children, mode } = props;\n  \n  \/\/ Type Guard \u578b\u30ac\u30fc\u30c9\n  if (mode === 'info'){\n    return(\n      \/\/ info mode JSX\n    );\n  }\n \n  \/\/ \u3053\u306e\u6bb5\u968e\u3067severity\u304c\u3042\u308b\u3053\u3068\u304c\u78ba\u5b9a\u3057\u3066\u3044\u308b\u306e\u3092TypeScript\u3082\u7406\u89e3\u3057\u3066\u3044\u308b\n  const { severity } = props;\n  return(\n    \/\/ warning mode JSX\n  );\n}","language":"typescript","id":2,"filename":""}

ラッパーで抽象化せずにインフォメーションと警告メッセージを表示するコンポーネントを別に分けた方が良いのでは、という議論は設計の問題であり、アプリケーションによるところですね。

合併型の使用法 まとめ

関数の定義時に型定義も行わなければならないTypeScriptでは、(積極的な理由がない場合でも)引数の型定義に合併型が使用されることがよくあります。

ツカレパンダ
ツカレパンダ

エラーが起こるかもって指摘されると疲れるよ。

ダテネコ
ダテネコ

実装しきってから分かるより、都度型チェックが入る方がずっといいんだぜ。

TypeScriptのエラーを正しく消す型ガードは、全体のリスクを軽減することに繫がる(と感じます)ので、うまく付き合っていきましょう。

まとめ 合併型の使用法と型ガード

合併型は「数種類の型のいずれかを受けたい時」に使われることが多い。

合併型を引数に取る関数では、型ガードを併用することが多い。

積極的な理由で合併型を採用した場合の型ガードは処理の分岐、消極的な理由の時は例外処理の意味合いが強い。

交差型はプロパティの”上書き”では無い

1章で見た通りに考えると、交差型は合併型よりも本来理解しやすい機能と思われます。

  • 2つ以上の型定義を組み合わせて積集合にあたる型を作る。交差型を使うと必ず型定義が狭くなる
再掲

型定義が狭くなるので、既存の型の継承はできても、型定義が広がる方向へプロパティの上書きをすることはできません。

誤解されがちな原因は、交差型のよくある用途が原因となっている気がします。使用例を通してみてみます。

build-inの型との交差型で型を定義する

交差型の用途として実用上ありそうな、HTML要素をラップするReactのラッパーコンポーネントを考えます。

下記は、ほぼ必ず一緒に使用されるlabel要素とinput要素をラッパーコンポーネントにまとめて、id属性で紐付けておくコンポーネントです。

{"code":"import { ComponentPropsWithoutRef } from 'react';\ntype InputProps = {\n  label: string;\n  id: string;\n} & ComponentPropsWithoutRef<'input'>;\n\/\/ ConponentPropsWithoutRef\u306eprops\u306f\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306edestructure\u3067\u5c55\u958b\u3057\u3066\u304a\u304f\nexport default function Input( { label, id, ...props }: InputProps ){\n  return(\n    <>\n      <label htmlFor={id}>{label}<\/label>\n      <input id={id} type=\"text\" { ...props } \/>\n    <\/>\n  );\n}","language":"tsx","id":17,"filename":""}
シバセンセー
シバセンセー

デザインシステムの実装か、統一的なHTML要素への機能付与とかに使えそうだね。

機能面はほとんどHTMLのinput要素なので、inputが受け取れるpropsを全て指定できる状態にしておきたいです。

そのため、reactライブラリに組み込み(build-in)で用意されているComponentPropsWithoutRefとの交差型をとり、<Input />コンポーネントがレンダーするinputのpropsとして展開しておきます。

build-in型の解説は本題ではないので割愛しますが、ComponentPropsWithoutRefはGenericsであり、HTMLタグの名前を指定してデフォルトのpropsを取得します

詳細は記載しませんが、スタイリングしたり、カスタムのイベントハンドラを渡したり、forwardRefで自身を参照させたりできる便利な<Input />コンポーネントができました。

<Input />の型定義を見てみます。

{"code":"import { ComponentPropsWithoutRef } from 'react';\ntype InputProps = {\n  label: string;\n  id: string;\n} & ComponentPropsWithoutRef<'input'>;","language":"tsx","id":17,"filename":""}

InputPropsは、HTMLのinputに指定できるpropsに加えて、カスタムのlabel, id2つが必須のpropsですので、型定義はComponentPropsWithoutRef<'input'>よりも狭くなっています。

<Input />コンポーネントの型定義

交差型の使用法を勘違いしそうなところ

前節の<Input />コンポーネントの使い方とカスタマイズ例を通して、交差型の実用で使用方法を勘違いしそうなところを見ていきます。

<Input />を使用する一例は、以下の疑似コードのようにフォームの中でレンダーする方法です。

{"code":"import { Input } from ...;\nexport default function Input( { label, id, ...props }: InputProps ){\n  return(\n    <>\n    <Form>\n       <Input  label='Name' id='name' type='' \/>\n      \/\/ ... other elements in Form\n      <\/Form>\n    <\/>\n  );\n}","language":"tsx","id":17,"filename":""}

このとき、<Input />input要素に対応したpropsを全て受け付けるため、typeを指定することができます。

ところで、HTML要素のinput要素は、type属性によって受け付けるデータの種類・見た目が大きく変わります。

input要素のtype属性による違い

これら全てに<Input />ラッパーコンポーネントで統一的なスタイリングや機能を与えるのは容易ではないので、仮に、見た目の似ているtext, email, passwordの3種類のtypeに限定したinputをラップするコンポーネントを作成したとしましょう。

{"code":"import { ComponentPropsWithoutRef } from 'react';\ntype TextInputProps = {\n  label: string;\n  id: string;\n  \/\/ type\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u30ab\u30b9\u30bf\u30e0\u3067\u5b9a\u3081\u308b\n  type: 'text' | 'email' | 'password';\n} & ComponentPropsWithoutRef<'input'>;\n\/\/ \u30c6\u30ad\u30b9\u30c8\u5165\u529b\u5168\u822c\u3092\u53d7\u3051\u4ed8\u3051\u308binput\u3092\u30e9\u30c3\u30d7\u3059\u308b\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\nexport default function TextInput( { label, id, type ...props }: TextInputProps ){\n  return(\n    <>\n      <label htmlFor={id}>{label}<\/label>\n      <input id={id} type={type} { ...props } \/>\n    <\/>\n  );\n}","language":"tsx","id":17,"filename":""}

この時、コンポーネントの型定義でComponentPropsWithoutRef<'input'>に含まれるtypeプロパティを上書きしているかのように見えてしまいますが、実際には違います。

typeプロパティはComponentPropsWithoutRef<'input'>内部で、型がReact.HTMLInputTypeAttributeまたはundefinedのオプショナルプロパティとして定義されています。

やっていることは、デフォルトの型定義と自身の型定義で交差型を取って型定義を厳しい方向に狭くしているだけです(下図の青い範囲)

typeプロパティの上書きではなく、単なる交差型

オプショナルプロパティ(Optional Property)
設定するかどうか任意のプロパティ。property? : typeのように記述される

交差型の定義を考えると当たり前ではありますが、内部の実装が見えないbuild-inの型との交差型をとった時に、プロパティを上書きしているかのように見えることがあるのが誤解しがちなポイントかと思います。

注意

build-in型でプロパティがどのように定義されているか気を付けないと、交差型をとった結果を誤解することがある

どうしてもプロパティを上書きしたい時はOmitする

プロジェクトの仕様などで、どうしてもbuild-in型を含む既存の型定義を広げる方向に書き換えたい場合は、型定義からプロパティを消去するOmitを使います。

実例を考えてみましょう。

build-in型の不要なプロパティを削除する

前節でも取り上げた、HTML要素のラッパーコンポーネント<Input />のカスタマイズを考えます。

<Input />を使用すると入力欄に必ずlabelが付与されるので、代わりにプレースホルダーは使わないということにプロジェクトで決定したとします。

アクセシビリティの観点からも、昨今ではプレースホルダーにはできる限り頼らないと言われているようですので、自然な考え方と言えますね。

もともと任意で指定できるplaceholderプロパティを除外した型定義に変更し、<CustomInput />コンポーネントとして作ってみます。

{"code":"import { ComponentProps } from \"react\";\n\/\/ ComponentProps<'input'>\u304b\u3089placeholder\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u524a\u9664\ntype NoPlaceholderInput = Omit<ComponentProps<'input'>, 'placeholder'>;\ntype CustomInputProps = {\n    label: string;\n    id: string\n} & NoPlaceholderInput;\nexport default function CustomInput({label, id, ...props}: CustomInputProps){\n    return (\n        <>\n            <label htmlFor={id}>{label}<\/label>\n            <input id={id} type=\"text\" {...props} \/>\n        <\/>\n    );\n}","language":"tsx","id":17,"filename":""}
Omit

Omit<T, Properties> | 型Tから、指定されたPropertiesを削除する

Omitは、もとの型と削除するプロパティを指定して使用します。

これで、CustomInputPropsplaceholderプロパティを持たない型になりましたので、placeholder propsを渡そうとすると無事にエラーになりました。

<CustomInput />に渡すpropsの型CustomInputPropsには、placeholderプロパティが無い

Omitを使用するとプロパティ自体が消えますが、<ComponentProps<‘input’> & { placeholder: never } のような形でセーフティに値を代入させない方法もあることに注意してください。

Omitしてから交差型を取るカスタマイズと、その一般化

既存の型定義を広げる方向にカスタマイズしたいが、単なるプロパティの削除ではなく、同じプロパティを使い続けたいという場合には、Omitで既存の型のプロパティを一度削除した後、自分でプロパティを再定義した型との交差型をとります。

想定コード例として、todoを保持するデータのIDに数値型だけを許容していたが、uuid()などで発生させる文字列もIDに使用できるよう、カスタムする必要が出てきた場合を書いてみます。

{"code":"type Todo = {\n  id: number;\n  title: string;\n  description: string;\n  progress: 'New' | 'Working' | 'Done';\n};\n\n\/\/ id\u30d7\u30ed\u30d1\u30c6\u30a3\u3092\u4e00\u65e6\u524a\u9664\u3057\u3066\u304b\u3089\u3001string \u307e\u305f\u306fnumber\u3068\u3057\u3066\u518d\u5b9a\u7fa9\ntype CustomTodo = Omit & {\n  id: number | string;\n};\nconst todo: CustomTodo = {\n  id: 'abcd-edgh-ijkl-mnop', \/\/ OK\n  title: 'Meet Client',\n  description: 'Have meeting with client about new project.',\n  progress: 'New'\n};","language":"typescript","id":2,"filename":""}

さらに、(定義を広げる方向に)カスタムしたいプロパティが複数あり、上書き操作をプロジェクト内で何度も使用する場合の一般化に言及します。

Genericsを使用して、複数のプロパティの上書きを行う専用の型を定義すると、容易にカスタムできるようになります。

{"code":"\/\/ Type T\u304b\u3089\u3001U\u306b\u6307\u5b9a\u3055\u308c\u305f\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306e\u30ad\u30fc\u3092\u5168\u3066\u524a\u9664\u3057\u3001U\u3068\u306e\u4ea4\u5dee\u578b\u3092\u53d6\u308b\u4e0a\u66f8\u304d\u5c02\u7528\u306e\u578b\ntype Merge = Omit & U\ntype Todo = {\n  id: number;\n  title: string;\n  description: string;\n  progress: 'New' | 'Working' | 'Done';\n}\n\/\/ Merge\u3092\u4f7f\u7528\u3057\u3066\u3001Todo\u578b\u306eid\u3068progress\u3092\u5e83\u3052\u308b\ntype CustomTodo = Merge\n\nconst customTodo: CustomTodo = {\n  id: 'abcd-edgh-ijkl-mnop', \/\/ OK\n  title: 'Meet Client',\n  description: 'Have meeting with client about new project.',\n  progress: 'Pending' \/\/ OK\n};","language":"typescript","id":2,"filename":""}

keyof Uの部分で、Genericsに渡したオブジェクトにループをかけていますね。

実際に実行してみると、プロパティの上書き前後での型定義の変化が分かります。

idに文字列を許可し、progressに’Pending’を許可したCustomTodo型では、同じデータでもエラーが出ていない

Omitを用いたプロパティの書き換えは、交差型の作用ではないことに注意してください。書き換えは以下の操作に分かれており、交差型は単に積集合を作っているに過ぎません。

  • Omitで既存の型のプロパティを消去する
  • 書き換えたいプロパティだけを持つ新しい型を自作する
  • 1と2の型で交差型をとる
 Merge型で一般化したプロパティの上書き操作を分解するとこうなる

個人的な意見ですが、Merge型に一般化した上書き操作は多用しない方が良いと思われます。Omitには潜在的な問題もある上、どうしても必要となる場面が限定的だからです。

・型定義を厳しくするカスタマイズは常に、単に交差型を使用すれば十分

・使用させたいくないpropsは、単にOmitすれば良い

・型定義を広げつつ、同名プロパティの使用にこだわると保守に課題がある

交差型の使用法と注意点 まとめ

交差型の使用法と注意点についてまとめます。

まとめ 交差型の使用法と注意点

交差型は「build-in型を使用したラッパーコンポーネントの型定義」に使われることが多い。

交差型の作用は積集合であり、常に型定義は狭くなる。

どうしても広げる方向にプロパティを上書きする時は、Omitを使う。

まとめ

最後に、本記事のまとめになります。お疲れ様でした。

合併型・交差型の使用法と注意点 まとめ
  • 合併型は和集合・交差型は積集合を作る機能である
  • 合併型を使用すると型定義は広くなり、交差型を使用すると型定義は狭くなる
  • 合併型は様々な場面で登場し、型ガードが必要なことが多い
  • 交差型は上書きではない。上書きにはOmitを使う

参考サイト・用語集

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


error: Content is protected !!