🍣

【RN】Omitを使ってTextStyleを独自仕様にするTips

2021/04/27に公開

はじめに

今回はReact Native + TypescriptTextコンポーネントのPropsを改造した際の内容を備忘録として残します。
Textに限らず、デフォルトのコンポーネントをラップした独自コンポーネントを作成することを考えている場合は役立つ内容かと思います。

前知識「Textの基本的な使い方」

React Native未経験の方のために簡単に説明させていただきます。
React Nativeにはデフォルトでいくつかのコンポーネントが用意されており、Textもそのひとつです。
以下のように使います。

<Text style={{fontSize: 20, color: "red"}} >ほげほげ</Text>

reactでいうところの<p>に近い使い勝手です。

Textをラップした独自コンポーネントを作りたい

ある日、TextをラップしたHogeTextというコンポーネントを作ることになりました。
事の経緯としては、アプリの規模が大きくなってきて「あちこちで異なるfontSizeTextが乱立」してきたことが原因です。
※この辺りの取り決めがチーム内でできておらず、「大きい文字」を人によっては24にしたり28にしたりと随分とちぐはぐなデザインになってきたための対策です。

HogeTextはプロパティとしてfontSizeを持ち、fontSize"default""large"のどちらかの値しか受け取れません。
fontSizeは省略可能で、その場合は"default"として扱われます。

使用イメージは以下の通りです。

<HogeText fontSize="large">大きな文字</HogeText>
<HogeText fontSize="default" >普通の文字</HogeText>
<HogeText>普通の文字2</HogeText>

単にTextをラップするだけなので簡単だと思っていましたが、Textにはstyleの他にもnumberOfLinesなどの有用なプロパティがあります。
HogeTextfontSize以外はTextと同じ使い勝手にしたいので、PropsとしてはTextProps型を持たせたいです。

import { TextProps } from 'react-native';

// HogeTextのプロパティ型
type HogeTextProps = {
  fontSize: "default" | "large"
} & TextProps

しかしTextPropsにはstyleが含まれており、style(型としてはTextStyle型)には当然fontSizeが含まれています。
従って、以下のような指定ができてしまいます。

// ↓これでは本末転倒
<HogeText fontSize="default" style={{fontSize: 100}} numberOfLines={1} >普通の文字</HogeText>

やりたいこととしては「基本はTextと同じ振る舞いなんだけど、stylefontSizeを指定できないTextを作りたい」というものです。

TypescriptのOmitを使う

解決策としてはTypescriptOmitを使いました。

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

Omit「指定した型から、特定のプロパティを除いた型を作る」 時に使います。

以下のようなイメージです。

// Hoge型
type Hoge = {
  a: string;
  b: number;
  c: boolean;
}

// Hoge型からaというプロパティを除外した型
type HogeRemovedA = Omit<Hoge, "a">;

const hoge: HogeRemovedA = {
  // ↓これは怒られる
  // a: "aaa"
  b: 10,
  c: false,
}

上記を踏まえて実際に実装したHogeTextとそのプロパティが以下の通りになります。

HogeText.tsx
import React from 'react';
import {StyleProp, TextProps, TextStyle, Text} from 'react-native';

/** TextStyleからfontSizeを除いたType */
type NoFontSizeTextStyle = Omit<TextStyle, 'fontSize'>;

/** TextPropsからstyleを除いたType */
type NoStyleTextProps = Omit<TextProps, 'style'>;

/** TextPropsの中にfontSizeを含まないstyleを持つType */
type NoFontSizeTextProps = NoStyleTextProps & {style?: StyleProp<NoFontSizeTextStyle>};

/** ワンライナーで書くとこんな感じ */
type NoFontSizeTextPropsOneLiner = Omit<TextProps, 'style'> & { style?: StyleProp<Omit<TextStyle,'fontSize'>>};

/** HogeTextの独自Propsを定義 */
type HogeTextProps = {
    fontSize: "default" | "large";
} & NoFontSizeTextProps;

const HogeText: React.FC<HogeTextProps> = (props) => {
    const fontSize: "default" | "large" = props?.fontSize || "default";
    // largeなら22,defaultなら18
    const fontSizeValue: number = fontSize === "large"? 22 : 18;

    return(
        <Text props={...props} style={{fontSize: fontSizeValue}}>
          {props.children}
        </Text>
    )
}

ポイントは以下の通りです。

  • TextStyleからfontSizeを除外した型を生成(NoFontSizeTextStyle)
  • TextPropsからstyleを除外した型を生成(NoStyleTextProps)
  • NoStyleTextPropsstyleというNoFontSizeTextStyle型のプロパティを持つ型を合成(NoFontSizeTextProps)
  • NoFontSizeTextPropsは基本はTextPropsだが、style.fontSizeだけがない

このHogeTextTextと同じように呼び出すことができますが、style.fontSizeを指定することはできず、文字サイズを変えたい場合はプロパティとして用意してあるfontSizeを指定("default""large"のみ受け付ける)する必要があります。

// ×これは怒られる
<HogeText style={{fontSize: 30}}>30</HogeText>
// ◯fontSize以外のスタイルや通常のTextPropsは指定可能
<HogeText style={{fontWeight: "bold"}} numberOfLines={1} >bold</HogeText>

あとは今までTextを使っていた箇所をHogeTextに置き換えて、それぞれ正しいfontSizeを指定すればOKです。

まとめ

今回はTextの型をいじって、独自に定義したfontSizeのみに対応するコンポーネントを作成しました。
アプリが巨大化するとデフォルトのコンポーネントをラップしたコンポーネントを作る機会も多いかと思います。

その際にデフォルトで用意されたプロパティの型を加工して、独自使用にすることができるため、意図しない挙動を防ぐことができます。

今回の内容が役立ちましたら幸いです。

Discussion