📚

Styled Componentsの型推論の仕組みについて調べる

2023/04/16に公開

始めに

ReactにはStyled Componentsがありますが、バッククォートでstyleを書いていくため型は当たらないだろうなと思っていました。しかし、${}で内挿したコードには以下みたいにpropsの型が当たっていて衝撃を受けました。なんでこんなことができるか気になったので調べてみました。

テンプレートリテラルの仕様

まずテンプレートリテラルで使われる「`」はメソッドの直後に書くことで括弧をつけずに文字列を渡すことができます。ただバッククォートで書くパターンは勝手に配列になるようなので引数もそのようにする必要があります。

バッククォートで実行する
const hello = ([name]: readonly string[]) => 'Hello,' + name;

// 2つは同じ処理をする
hello(['Taro']);
hello`Taro`

この仕様までは知っていたのですが、実は${}で内挿する場合は引数が分かれるようです。
引数が分かれるため、以下のように内挿する値も型指定できます。

内挿も含めて型をつける
const myTag = (strings: readonly string[], personExp: string, ageExp: number) => {
  const str0 = strings[0]; // "That "
  const str1 = strings[1]; // " is a "
  const str2 = strings[2]; // "."

  const ageStr = ageExp > 99 ? "centenarian" : "youngster";

  // We can even return a string built using a template literal
  return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const person = "Mike";
const age = 28;
const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.

この結果から分かるように、第一引数は純粋な文字列部分の配列で、${}で内挿される箇所で分割されています。分割された箇所に内挿する項目は第二引数、第三引数と可変長引数になっているので、以下のように書くとループで全て結合させることができます。

内挿をループでいい感じに結合させる
const myTag = (strings: readonly string[], ...values: [string, number]) => {
  return strings[0] + strings.slice(1).reduce((str, currentStr, index) => {
    return str + values[index] + currentStr;
  }, '');
}

Styled Componentsに当てはめてみる

上記の仕様を踏まえてstyled-componentsの実装を簡単に再現すると以下のようになります。

styled-componentsの簡易実装
import { FC, ReactNode } from "react";

let countStyle = 0;

export const myStyledDiv = function <Props = {}>(
  styles: ReadonlyArray<string>,
  ...values: Array<(props: Props) => string | undefined>
): FC<Props & { children: ReactNode }> {
  return (props) => {
    const styleStr =
      styles[0] +
      styles.slice(1).reduce((styleStr, currentStyle, index) => {
        return styleStr + values[index](props) + currentStyle;
      }, "");

    const className = `my-styled-${countStyle++}`;
    const css = `.${className} { ${styleStr} }`;
    return (
      <>
        <style>{css}</style>
        <div className={className}>{props.children}</div>
      </>
    );
  };
};

このメソッドを使った例は以下のようになり、styled-componentsと全く同じ書き方ができます。${}内にあるpropsもちゃんと型が当たります。

styled-componentsの簡易実装の使用例
import { FC } from "react";
import { myStyledDiv } from "../myStyled";

const Title = myStyledDiv`
  font-size: 20px;
  font-weight: bold;
  margin-bottom: 10px;
  text-decoration: underline;
`;

const Text = myStyledDiv<{ primary?: boolean }>`
  letter-spacing: 0.05em;
  color: ${({ primary }) => (primary ? "blue" : undefined)}
`;

export const MyStyledComponent: FC = () => {
  return (
    <div>
      <Title>自作Styled Components</Title>
      <Text primary>Primary テキスト</Text>
      <Text>Normal テキスト</Text>
    </div>
  );
};

終わりに

以上がStyled Componentsの型推論の仕組みでした。まさか内挿する要素ごとに引数として切り出されているとは思いもしませんでした。JavaScriptの仕様を駆使したまでと言えばそれまでですが、大分クレイジーなことしているなぁと思いました(笑)。
最後に検証としてCodeSandboxでサンプルを書いていますので、興味がある方はこちらもご参照ください。

参考記事

https://jsnotice.com/posts/2019-09-19/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates
https://stackoverflow.com/questions/29660381/backticks-calling-a-function-in-javascript
https://qiita.com/murasuke/items/8568b1629a08f89dcf06

Discussion