APIからあるオブジェクトが渡され、フロントでデータが無ければデフォルト値を付けるような処理を TypeScript で書いていてハマったのでメモ。
※ フロントは React です
API から渡されるデータ
InitUnitData = { first: { id: 1, name: '春風 わかば', type: 'cute', }, second: { id: 2, name: '姫石 らき' } }
type
が無ければデフォルト値を付けたい
Array.reduce でオブジェクトをフォーマットする
JavaScript で書くとこんな感じ。
const DefaultType = 'cute'; function Unit({ InitUnitData }) { return <UnitContainer {...formatUnitData(InitUnitData)} /> } function formatUnitData(unitData) { return Object.keys(unitData).reduce((acc, key) => { const { type, ...data } = unitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {}); }
Array.reduce
の初期値を空オブジェクト {}
にして、それにデフォルト値を設定したデータを追加していけばOKって感じ。至ってフツーの書き方かな〜と思っています。
この処理を TypeScript で書こうとしてハマっていました。
TypeScript で Array.reduce でオブジェクトのデフォルト値を設定する
APIから渡されるデータの型と、コンポーネントで使用するデータの型
APIから渡されるデータは type があったりなかったりするので type?: string
ですが、コンポーネントで使用する型は type が設定されている前提なので type: string
になります。
フォーマットされたデータを扱うコンポーネント
// UnitContainer.tsx export type IdolDataType = { id: number; name: string; type: string; }; export type UnitDataType = { first: IdolDataType; second: IdolDataType; }; type UnitContainerProps = { unitData: UnitDataType }; export default function UnitContainer({ unitData }: UnitContainerProps) {...}
APIからデータを受け取って整形してコンポーネントに流し込む箇所
// Unit.tsx import UnitContainer, { IdolDataType, UnitDataType, } from './UnitContainer'; type InitIdolDataType = Omit<IdolDataType, 'type'> & { type?: string; }; type InitUnitDataType = { first: InitIdolDataType; second: InitIdolDataType; } const DefaultType = 'cute'; type UnitProps = { initUnitData: InitUnitDataType; }; export default function Unit({ initUnitData }: UnitProps) { return <UnitContainer ...formatUnitData(initUnitData) />; }
つまりデータを整形する formatUnitData
関数は、 InitUnitDataType
型を受取り UnitDataType
型を返す必要があります。
Array.reduce でオブジェクトを整形する部分の型指定
JavaScript 版ではこうでした
function formatUnitData( initUnitData ) { return Object.keys(unitData).reduce((acc, key) => { const { type, ...data } = initUnitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {}); }
関数の引数と返る値に型を付けると function formatUnitData( initUnitData: InitUnitDataType ): UnitDataType
こうなります。
1. return
されるオブジェクトの型が不明
Array.reduce
は第二引数 (acc
の初期値) が戻り値の型推論されるので、 初期値に空オブジェクト {}
を渡すと {}
が返されると推論されてしまうので、関数の返すべき型と合ってないとなる。as
で初期値の型を指定する
function formatUnitData( initUnitData: InitUnitDataType ): UnitDataType { return Object.keys(unitData).reduce((acc, key) => { const { type, ...data } = initUnitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {} as UnitDataType); }
2. Array.reduce
内の関数に渡される key
が any
と判断されるので initUnitData[key]
が何か推論できない
initUnitData[key]
の部分で Element implicitly has as 'any' type because ...
が出されます。
これは TypeScript はオブジェクトのプロパティに変数を使ったブランケット記法 (Object[key]
) でアクセスすると、コンパイラは何型が帰ってくるか推定できずエラーとなってしまうようです。
keyof
を使って Object.keys(unitData)
が InitUnitDataType
のキーの配列で、key
には InitUnitDataType
のキーのいずれかも文字列しか入らない事を明示する
function formatUnitData( initUnitData: InitUnitDataType ): UnitDataType { return (Object.keys(unitData) as Array<keyof InitUnitDataType>) .reduce((acc, key) => { const { type, ...data } = unitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {} as UnitDataType ); }
これで key
は 'first' | 'second'
になるので unitData[key]
が何型を持つかわからないというエラーは解消されます。
3. Array.reduce
内の acc
は初回ループ時に実態と型が合っていない状態になっている。
2.の段階で基本的にコンパイラのエラーは消えているのですが、Array.reduce
で渡される acc
空オブジェクトで、acc.first
, acc.second
は型上は IdolDataType
と推論されるけど、実際のデータでは undefind
と乖離しているので Partial<UnitDataType>
として undefind
の含むようにした方が良いと教えてもらいました。
Partial
型 T のすべてのプロパティを省略可能(つまり| undefined
)にした新しい型を返す Mapped Type です。interface Foo { bar: number baz: boolean } type PartialFoo = Partialcf. TypeScript特有の組み込み型関数 #Partial// PartialFoo { // bar?: number // baz?: boolean // }
Array.reduce
の初期値を {} as Partial<UnitDataType>
とすれば acc
は {first?: IdolDataType, second?: IdolDataType}
という型として扱われるようになるっぽい。
function formatUnitData( initUnitData: InitUnitDataType ): UnitDataType { return (Object.keys(unitData) as Array<keyof InitUnitDataType>) .reduce((acc, key) => { const { type, ...data } = unitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {} as Partial<UnitDataType> ); }
こうすると return
されるオブジェクトが {}
の可能性を含んでしまってコンパイラにエラーを言われるので return
されるオブジェクトは UnitDataType
型だと明示します
function formatUnitData( initUnitData: InitUnitDataType ): UnitDataType { return (Object.keys(unitData) as Array<keyof InitUnitDataType>) .reduce((acc, key) => { const { type, ...data } = unitData[key]; return { ...acc, [key]: { ...data, type: type || DefaultType, } }; }, {} as Partial<UnitDataType> ) as UnitDataType; }
これで、TSlint を通るようになりました。
所感
個人的にはただオブジェクトにプロパティが無ければ初期値を付けたかっただけなのに、コンパイラが理解できるように厳密に型を保証しようとすると冗長になってしまうな〜という印象でした。
プロパティを | undefind
に変換できる Partial
や部分的に除く Omit
など TypeScript 特有の関数知らないものが多く、JavaScript で考えて TypeScript に変換しようとしてしまうと今回のようにハマってしまうから、最初からコンパイラの頭で TypeScript で考えれるようにならないと、ロジックを考える時間より、TSlint が通るようにする作業のほうが多く時間がかかってしまって健全じゃないから、ちゃんと TypeScript 学習しておかないと今後キツイな〜と実感しました。
ガンバロ…
[参考]
- Advanced Types · TypeScript
- TypeScript特有の組み込み型関数 - log.pocka.io
- Typescript ブラケット記法(Object[key])でno index signatureエラーをtype safeに解決したい。 - aknow2
- 【TypeScript】 Object[key]() (ブラケット記法)で関数呼び出ししたら Element implicitly has an 'any' type でハマった話 - Qiita
- Array.prototype.reduce() - JavaScript | MDN
- 発売日: 2009/06/03
- メディア: Blu-ray